hackmyagent 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/README.md +66 -28
- package/dist/arp/cli/index.d.ts +3 -0
- package/dist/arp/cli/index.d.ts.map +1 -0
- package/dist/arp/cli/index.js +219 -0
- package/dist/arp/cli/index.js.map +1 -0
- package/dist/arp/config/loader.d.ts +8 -0
- package/dist/arp/config/loader.d.ts.map +1 -0
- package/dist/arp/config/loader.js +102 -0
- package/dist/arp/config/loader.js.map +1 -0
- package/dist/arp/enforcement/kill-switch.d.ts +22 -0
- package/dist/arp/enforcement/kill-switch.d.ts.map +1 -0
- package/dist/arp/enforcement/kill-switch.js +122 -0
- package/dist/arp/enforcement/kill-switch.js.map +1 -0
- package/dist/arp/engine/event-engine.d.ts +29 -0
- package/dist/arp/engine/event-engine.d.ts.map +1 -0
- package/dist/arp/engine/event-engine.js +233 -0
- package/dist/arp/engine/event-engine.js.map +1 -0
- package/dist/arp/index.d.ts +81 -0
- package/dist/arp/index.d.ts.map +1 -0
- package/dist/arp/index.js +239 -0
- package/dist/arp/index.js.map +1 -0
- package/dist/arp/intelligence/adapters.d.ts +45 -0
- package/dist/arp/intelligence/adapters.d.ts.map +1 -0
- package/dist/arp/intelligence/adapters.js +222 -0
- package/dist/arp/intelligence/adapters.js.map +1 -0
- package/dist/arp/intelligence/anomaly.d.ts +32 -0
- package/dist/arp/intelligence/anomaly.d.ts.map +1 -0
- package/dist/arp/intelligence/anomaly.js +80 -0
- package/dist/arp/intelligence/anomaly.js.map +1 -0
- package/dist/arp/intelligence/budget.d.ts +33 -0
- package/dist/arp/intelligence/budget.d.ts.map +1 -0
- package/dist/arp/intelligence/budget.js +150 -0
- package/dist/arp/intelligence/budget.js.map +1 -0
- package/dist/arp/intelligence/coordinator.d.ts +43 -0
- package/dist/arp/intelligence/coordinator.d.ts.map +1 -0
- package/dist/arp/intelligence/coordinator.js +301 -0
- package/dist/arp/intelligence/coordinator.js.map +1 -0
- package/dist/arp/interceptors/a2a-protocol.d.ts +29 -0
- package/dist/arp/interceptors/a2a-protocol.d.ts.map +1 -0
- package/dist/arp/interceptors/a2a-protocol.js +111 -0
- package/dist/arp/interceptors/a2a-protocol.js.map +1 -0
- package/dist/arp/interceptors/filesystem.d.ts +33 -0
- package/dist/arp/interceptors/filesystem.d.ts.map +1 -0
- package/dist/arp/interceptors/filesystem.js +199 -0
- package/dist/arp/interceptors/filesystem.js.map +1 -0
- package/dist/arp/interceptors/mcp-protocol.d.ts +25 -0
- package/dist/arp/interceptors/mcp-protocol.d.ts.map +1 -0
- package/dist/arp/interceptors/mcp-protocol.js +126 -0
- package/dist/arp/interceptors/mcp-protocol.js.map +1 -0
- package/dist/arp/interceptors/network.d.ts +26 -0
- package/dist/arp/interceptors/network.d.ts.map +1 -0
- package/dist/arp/interceptors/network.js +146 -0
- package/dist/arp/interceptors/network.js.map +1 -0
- package/dist/arp/interceptors/process.d.ts +26 -0
- package/dist/arp/interceptors/process.d.ts.map +1 -0
- package/dist/arp/interceptors/process.js +157 -0
- package/dist/arp/interceptors/process.js.map +1 -0
- package/dist/arp/interceptors/prompt.d.ts +29 -0
- package/dist/arp/interceptors/prompt.d.ts.map +1 -0
- package/dist/arp/interceptors/prompt.js +82 -0
- package/dist/arp/interceptors/prompt.js.map +1 -0
- package/dist/arp/license/index.d.ts +59 -0
- package/dist/arp/license/index.d.ts.map +1 -0
- package/dist/arp/license/index.js +78 -0
- package/dist/arp/license/index.js.map +1 -0
- package/dist/arp/monitors/filesystem.d.ts +21 -0
- package/dist/arp/monitors/filesystem.d.ts.map +1 -0
- package/dist/arp/monitors/filesystem.js +141 -0
- package/dist/arp/monitors/filesystem.js.map +1 -0
- package/dist/arp/monitors/network.d.ts +32 -0
- package/dist/arp/monitors/network.d.ts.map +1 -0
- package/dist/arp/monitors/network.js +301 -0
- package/dist/arp/monitors/network.js.map +1 -0
- package/dist/arp/monitors/process.d.ts +24 -0
- package/dist/arp/monitors/process.d.ts.map +1 -0
- package/dist/arp/monitors/process.js +205 -0
- package/dist/arp/monitors/process.js.map +1 -0
- package/dist/arp/patterns/ai-threats.d.ts +48 -0
- package/dist/arp/patterns/ai-threats.d.ts.map +1 -0
- package/dist/arp/patterns/ai-threats.js +215 -0
- package/dist/arp/patterns/ai-threats.js.map +1 -0
- package/dist/arp/proxy/forward.d.ts +23 -0
- package/dist/arp/proxy/forward.d.ts.map +1 -0
- package/dist/arp/proxy/forward.js +152 -0
- package/dist/arp/proxy/forward.js.map +1 -0
- package/dist/arp/proxy/server.d.ts +45 -0
- package/dist/arp/proxy/server.d.ts.map +1 -0
- package/dist/arp/proxy/server.js +331 -0
- package/dist/arp/proxy/server.js.map +1 -0
- package/dist/arp/reporting/local-log.d.ts +22 -0
- package/dist/arp/reporting/local-log.d.ts.map +1 -0
- package/dist/arp/reporting/local-log.js +116 -0
- package/dist/arp/reporting/local-log.js.map +1 -0
- package/dist/arp/types.d.ts +230 -0
- package/dist/arp/types.d.ts.map +1 -0
- package/dist/arp/types.js +4 -0
- package/dist/arp/types.js.map +1 -0
- package/dist/attack/custom-payloads.d.ts +11 -0
- package/dist/attack/custom-payloads.d.ts.map +1 -0
- package/dist/attack/custom-payloads.js +108 -0
- package/dist/attack/custom-payloads.js.map +1 -0
- package/dist/attack/fail-policy.d.ts +16 -0
- package/dist/attack/fail-policy.d.ts.map +1 -0
- package/dist/attack/fail-policy.js +36 -0
- package/dist/attack/fail-policy.js.map +1 -0
- package/dist/attack/index.d.ts +12 -0
- package/dist/attack/index.d.ts.map +1 -0
- package/dist/attack/index.js +30 -0
- package/dist/attack/index.js.map +1 -0
- package/dist/attack/payloads/a2a-attacks.d.ts +12 -0
- package/dist/attack/payloads/a2a-attacks.d.ts.map +1 -0
- package/dist/attack/payloads/a2a-attacks.js +221 -0
- package/dist/attack/payloads/a2a-attacks.js.map +1 -0
- package/dist/attack/payloads/capability-abuse.d.ts +8 -0
- package/dist/attack/payloads/capability-abuse.d.ts.map +1 -0
- package/dist/attack/payloads/capability-abuse.js +222 -0
- package/dist/attack/payloads/capability-abuse.js.map +1 -0
- package/dist/attack/payloads/context-manipulation.d.ts +8 -0
- package/dist/attack/payloads/context-manipulation.d.ts.map +1 -0
- package/dist/attack/payloads/context-manipulation.js +217 -0
- package/dist/attack/payloads/context-manipulation.js.map +1 -0
- package/dist/attack/payloads/data-exfiltration.d.ts +8 -0
- package/dist/attack/payloads/data-exfiltration.d.ts.map +1 -0
- package/dist/attack/payloads/data-exfiltration.js +249 -0
- package/dist/attack/payloads/data-exfiltration.js.map +1 -0
- package/dist/attack/payloads/index.d.ts +29 -0
- package/dist/attack/payloads/index.d.ts.map +1 -0
- package/dist/attack/payloads/index.js +76 -0
- package/dist/attack/payloads/index.js.map +1 -0
- package/dist/attack/payloads/jailbreak.d.ts +8 -0
- package/dist/attack/payloads/jailbreak.d.ts.map +1 -0
- package/dist/attack/payloads/jailbreak.js +265 -0
- package/dist/attack/payloads/jailbreak.js.map +1 -0
- package/dist/attack/payloads/mcp-exploitation.d.ts +12 -0
- package/dist/attack/payloads/mcp-exploitation.d.ts.map +1 -0
- package/dist/attack/payloads/mcp-exploitation.js +221 -0
- package/dist/attack/payloads/mcp-exploitation.js.map +1 -0
- package/dist/attack/payloads/prompt-injection.d.ts +8 -0
- package/dist/attack/payloads/prompt-injection.d.ts.map +1 -0
- package/dist/attack/payloads/prompt-injection.js +262 -0
- package/dist/attack/payloads/prompt-injection.js.map +1 -0
- package/dist/attack/scanner.d.ts +84 -0
- package/dist/attack/scanner.d.ts.map +1 -0
- package/dist/attack/scanner.js +509 -0
- package/dist/attack/scanner.js.map +1 -0
- package/dist/attack/types.d.ts +153 -0
- package/dist/attack/types.d.ts.map +1 -0
- package/dist/attack/types.js +46 -0
- package/dist/attack/types.js.map +1 -0
- package/dist/benchmarks/index.d.ts +16 -0
- package/dist/benchmarks/index.d.ts.map +1 -0
- package/dist/benchmarks/index.js +27 -0
- package/dist/benchmarks/index.js.map +1 -0
- package/dist/benchmarks/oasb-1.d.ts +112 -0
- package/dist/benchmarks/oasb-1.d.ts.map +1 -0
- package/dist/benchmarks/oasb-1.js +1124 -0
- package/dist/benchmarks/oasb-1.js.map +1 -0
- package/dist/checker/check-skill.d.ts +48 -0
- package/dist/checker/check-skill.d.ts.map +1 -0
- package/dist/checker/check-skill.js +105 -0
- package/dist/checker/check-skill.js.map +1 -0
- package/dist/checker/index.d.ts +12 -0
- package/dist/checker/index.d.ts.map +1 -0
- package/dist/checker/index.js +16 -0
- package/dist/checker/index.js.map +1 -0
- package/dist/checker/permission-analyzer.d.ts +12 -0
- package/dist/checker/permission-analyzer.d.ts.map +1 -0
- package/dist/checker/permission-analyzer.js +84 -0
- package/dist/checker/permission-analyzer.js.map +1 -0
- package/dist/checker/publisher-verifier.d.ts +34 -0
- package/dist/checker/publisher-verifier.d.ts.map +1 -0
- package/dist/checker/publisher-verifier.js +121 -0
- package/dist/checker/publisher-verifier.js.map +1 -0
- package/dist/checker/skill-identifier.d.ts +14 -0
- package/dist/checker/skill-identifier.d.ts.map +1 -0
- package/dist/checker/skill-identifier.js +55 -0
- package/dist/checker/skill-identifier.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +3534 -0
- package/dist/cli.js.map +1 -0
- package/dist/hardening/index.d.ts +7 -0
- package/dist/hardening/index.d.ts.map +1 -0
- package/dist/hardening/index.js +9 -0
- package/dist/hardening/index.js.map +1 -0
- package/dist/hardening/scanner.d.ts +147 -0
- package/dist/hardening/scanner.d.ts.map +1 -0
- package/dist/hardening/scanner.js +5445 -0
- package/dist/hardening/scanner.js.map +1 -0
- package/dist/hardening/security-check.d.ts +85 -0
- package/dist/hardening/security-check.d.ts.map +1 -0
- package/dist/hardening/security-check.js +6 -0
- package/dist/hardening/security-check.js.map +1 -0
- package/dist/index.d.ts +38 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +91 -3525
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +10 -10
- package/dist/mcp-server.js.map +1 -1
- package/dist/oasb/config/dvaa-targets.d.ts +13 -0
- package/dist/oasb/config/dvaa-targets.d.ts.map +1 -0
- package/dist/oasb/config/dvaa-targets.js +89 -0
- package/dist/oasb/config/dvaa-targets.js.map +1 -0
- package/dist/oasb/harness/arp-wrapper.d.ts +29 -0
- package/dist/oasb/harness/arp-wrapper.d.ts.map +1 -0
- package/dist/oasb/harness/arp-wrapper.js +134 -0
- package/dist/oasb/harness/arp-wrapper.js.map +1 -0
- package/dist/oasb/harness/dvaa-client.d.ts +46 -0
- package/dist/oasb/harness/dvaa-client.d.ts.map +1 -0
- package/dist/oasb/harness/dvaa-client.js +98 -0
- package/dist/oasb/harness/dvaa-client.js.map +1 -0
- package/dist/oasb/harness/dvaa-manager.d.ts +17 -0
- package/dist/oasb/harness/dvaa-manager.d.ts.map +1 -0
- package/dist/oasb/harness/dvaa-manager.js +132 -0
- package/dist/oasb/harness/dvaa-manager.js.map +1 -0
- package/dist/oasb/harness/event-collector.d.ts +33 -0
- package/dist/oasb/harness/event-collector.d.ts.map +1 -0
- package/dist/oasb/harness/event-collector.js +86 -0
- package/dist/oasb/harness/event-collector.js.map +1 -0
- package/dist/oasb/harness/metrics.d.ts +14 -0
- package/dist/oasb/harness/metrics.d.ts.map +1 -0
- package/dist/oasb/harness/metrics.js +56 -0
- package/dist/oasb/harness/metrics.js.map +1 -0
- package/dist/oasb/harness/mock-llm-adapter.d.ts +34 -0
- package/dist/oasb/harness/mock-llm-adapter.d.ts.map +1 -0
- package/dist/oasb/harness/mock-llm-adapter.js +69 -0
- package/dist/oasb/harness/mock-llm-adapter.js.map +1 -0
- package/dist/oasb/harness/types.d.ts +74 -0
- package/dist/oasb/harness/types.d.ts.map +1 -0
- package/dist/oasb/harness/types.js +3 -0
- package/dist/oasb/harness/types.js.map +1 -0
- package/dist/plugins/core.d.ts +109 -0
- package/dist/plugins/core.d.ts.map +1 -0
- package/dist/plugins/core.js +30 -0
- package/dist/plugins/core.js.map +1 -0
- package/dist/plugins/credvault.d.ts +22 -0
- package/dist/plugins/credvault.d.ts.map +1 -0
- package/dist/plugins/credvault.js +374 -0
- package/dist/plugins/credvault.js.map +1 -0
- package/dist/plugins/signcrypt.d.ts +27 -0
- package/dist/plugins/signcrypt.d.ts.map +1 -0
- package/dist/plugins/signcrypt.js +317 -0
- package/dist/plugins/signcrypt.js.map +1 -0
- package/dist/plugins/skillguard.d.ts +25 -0
- package/dist/plugins/skillguard.d.ts.map +1 -0
- package/dist/plugins/skillguard.js +346 -0
- package/dist/plugins/skillguard.js.map +1 -0
- package/dist/registry/client.d.ts +125 -0
- package/dist/registry/client.d.ts.map +1 -0
- package/dist/registry/client.js +308 -0
- package/dist/registry/client.js.map +1 -0
- package/dist/registry/index.d.ts +3 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +10 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/scanner/external-scanner.d.ts +13 -0
- package/dist/scanner/external-scanner.d.ts.map +1 -0
- package/dist/scanner/external-scanner.js +299 -0
- package/dist/scanner/external-scanner.js.map +1 -0
- package/dist/scanner/index.d.ts +6 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +9 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/types.d.ts +32 -0
- package/dist/scanner/types.d.ts.map +1 -0
- package/dist/scanner/types.js +6 -0
- package/dist/scanner/types.js.map +1 -0
- package/dist/semantic/deep-scan.d.ts +13 -0
- package/dist/semantic/deep-scan.d.ts.map +1 -0
- package/dist/semantic/deep-scan.js +63 -0
- package/dist/semantic/deep-scan.js.map +1 -0
- package/dist/semantic/index.d.ts +17 -0
- package/dist/semantic/index.d.ts.map +1 -0
- package/dist/semantic/index.js +39 -0
- package/dist/semantic/index.js.map +1 -0
- package/dist/semantic/integration/cost-estimator.d.ts +17 -0
- package/dist/semantic/integration/cost-estimator.d.ts.map +1 -0
- package/dist/semantic/integration/cost-estimator.js +54 -0
- package/dist/semantic/integration/cost-estimator.js.map +1 -0
- package/dist/semantic/integration/finding-adapter.d.ts +34 -0
- package/dist/semantic/integration/finding-adapter.d.ts.map +1 -0
- package/dist/semantic/integration/finding-adapter.js +41 -0
- package/dist/semantic/integration/finding-adapter.js.map +1 -0
- package/dist/semantic/integration/oasb-upgrader.d.ts +20 -0
- package/dist/semantic/integration/oasb-upgrader.d.ts.map +1 -0
- package/dist/semantic/integration/oasb-upgrader.js +47 -0
- package/dist/semantic/integration/oasb-upgrader.js.map +1 -0
- package/dist/semantic/llm/budget.d.ts +50 -0
- package/dist/semantic/llm/budget.d.ts.map +1 -0
- package/dist/semantic/llm/budget.js +139 -0
- package/dist/semantic/llm/budget.js.map +1 -0
- package/dist/semantic/llm/cache.d.ts +36 -0
- package/dist/semantic/llm/cache.d.ts.map +1 -0
- package/dist/semantic/llm/cache.js +103 -0
- package/dist/semantic/llm/cache.js.map +1 -0
- package/dist/semantic/llm/client.d.ts +49 -0
- package/dist/semantic/llm/client.d.ts.map +1 -0
- package/dist/semantic/llm/client.js +64 -0
- package/dist/semantic/llm/client.js.map +1 -0
- package/dist/semantic/llm/index.d.ts +33 -0
- package/dist/semantic/llm/index.d.ts.map +1 -0
- package/dist/semantic/llm/index.js +129 -0
- package/dist/semantic/llm/index.js.map +1 -0
- package/dist/semantic/llm/prompts.d.ts +30 -0
- package/dist/semantic/llm/prompts.d.ts.map +1 -0
- package/dist/semantic/llm/prompts.js +120 -0
- package/dist/semantic/llm/prompts.js.map +1 -0
- package/dist/semantic/structural/credential-context.d.ts +14 -0
- package/dist/semantic/structural/credential-context.d.ts.map +1 -0
- package/dist/semantic/structural/credential-context.js +295 -0
- package/dist/semantic/structural/credential-context.js.map +1 -0
- package/dist/semantic/structural/index.d.ts +28 -0
- package/dist/semantic/structural/index.d.ts.map +1 -0
- package/dist/semantic/structural/index.js +138 -0
- package/dist/semantic/structural/index.js.map +1 -0
- package/dist/semantic/structural/instruction.d.ts +19 -0
- package/dist/semantic/structural/instruction.d.ts.map +1 -0
- package/dist/semantic/structural/instruction.js +167 -0
- package/dist/semantic/structural/instruction.js.map +1 -0
- package/dist/semantic/structural/mcp-config.d.ts +22 -0
- package/dist/semantic/structural/mcp-config.d.ts.map +1 -0
- package/dist/semantic/structural/mcp-config.js +294 -0
- package/dist/semantic/structural/mcp-config.js.map +1 -0
- package/dist/semantic/structural/permission-model.d.ts +16 -0
- package/dist/semantic/structural/permission-model.d.ts.map +1 -0
- package/dist/semantic/structural/permission-model.js +121 -0
- package/dist/semantic/structural/permission-model.js.map +1 -0
- package/dist/semantic/types.d.ts +122 -0
- package/dist/semantic/types.d.ts.map +1 -0
- package/dist/semantic/types.js +10 -0
- package/dist/semantic/types.js.map +1 -0
- package/package.json +25 -14
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3534 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* HackMyAgent CLI
|
|
5
|
+
* Find it. Break it. Fix it.
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
const commander_1 = require("commander");
|
|
42
|
+
const index_1 = require("./index");
|
|
43
|
+
const program = new commander_1.Command();
|
|
44
|
+
// Check for NO_COLOR env or non-TTY to disable colors by default
|
|
45
|
+
const noColorEnv = process.env.NO_COLOR !== undefined || process.stdout.isTTY === false;
|
|
46
|
+
// Color codes - will be cleared if --no-color is passed
|
|
47
|
+
let colors = {
|
|
48
|
+
green: '\x1b[32m',
|
|
49
|
+
yellow: '\x1b[33m',
|
|
50
|
+
red: '\x1b[31m',
|
|
51
|
+
brightRed: '\x1b[91m',
|
|
52
|
+
cyan: '\x1b[36m',
|
|
53
|
+
reset: '\x1b[0m',
|
|
54
|
+
};
|
|
55
|
+
if (noColorEnv) {
|
|
56
|
+
colors = { green: '', yellow: '', red: '', brightRed: '', cyan: '', reset: '' };
|
|
57
|
+
}
|
|
58
|
+
// Deprecation warning for removed HMAC auth
|
|
59
|
+
if (process.env.HMA_COMMUNITY_SECRET) {
|
|
60
|
+
console.error('Warning: HMA_COMMUNITY_SECRET is deprecated and no longer used. Scan tokens are now issued automatically.');
|
|
61
|
+
}
|
|
62
|
+
program
|
|
63
|
+
.name('hackmyagent')
|
|
64
|
+
.description(`Find it. Break it. Fix it.
|
|
65
|
+
|
|
66
|
+
The hacker's toolkit for AI agents. 147+ security checks, 75 attack
|
|
67
|
+
payloads, auto-fix with rollback, and OASB benchmark compliance.
|
|
68
|
+
|
|
69
|
+
Documentation: https://hackmyagent.com/docs
|
|
70
|
+
|
|
71
|
+
Updates (v${index_1.VERSION}):
|
|
72
|
+
- MCP JSON-RPC and A2A protocol attack modes
|
|
73
|
+
- SARIF and HTML output for all scan modes
|
|
74
|
+
- Semantic engine (structural + LLM analysis)
|
|
75
|
+
- OpenA2A Registry integration for trust scoring
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
$ hackmyagent secure Find vulnerabilities (147+ checks)
|
|
79
|
+
$ hackmyagent attack --local Break it with 75 attack payloads
|
|
80
|
+
$ hackmyagent secure --fix Fix issues automatically
|
|
81
|
+
$ hackmyagent fix-all Run all security plugins
|
|
82
|
+
$ hackmyagent scan example.com Scan external infrastructure`)
|
|
83
|
+
.version(index_1.VERSION, '-V, --version', 'Output the version number')
|
|
84
|
+
.option('--no-color', 'Disable colored output (also respects NO_COLOR env)')
|
|
85
|
+
.hook('preAction', (thisCommand) => {
|
|
86
|
+
const opts = thisCommand.opts();
|
|
87
|
+
if (opts.color === false) {
|
|
88
|
+
colors = { green: '', yellow: '', red: '', brightRed: '', cyan: '', reset: '' };
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
// Risk level colors and symbols
|
|
92
|
+
const RISK_DISPLAY = {
|
|
93
|
+
low: { symbol: '✅', color: () => colors.green },
|
|
94
|
+
medium: { symbol: '⚠️', color: () => colors.yellow },
|
|
95
|
+
high: { symbol: '🔴', color: () => colors.red },
|
|
96
|
+
critical: { symbol: '🚨', color: () => colors.brightRed },
|
|
97
|
+
};
|
|
98
|
+
const RESET = () => colors.reset;
|
|
99
|
+
program
|
|
100
|
+
.command('check')
|
|
101
|
+
.description(`Verify a skill before installing
|
|
102
|
+
|
|
103
|
+
Analyzes skill safety by checking:
|
|
104
|
+
• Publisher identity via DNS TXT records
|
|
105
|
+
• Permissions requested (filesystem, network, shell)
|
|
106
|
+
• Revocation status against global blocklist
|
|
107
|
+
|
|
108
|
+
Risk levels: low, medium, high, critical
|
|
109
|
+
Exit code 1 if high/critical risk detected.
|
|
110
|
+
|
|
111
|
+
Examples:
|
|
112
|
+
$ hackmyagent check @anthropic/claude-mcp
|
|
113
|
+
$ hackmyagent check @publisher/skill --verbose
|
|
114
|
+
$ hackmyagent check @publisher/skill --json`)
|
|
115
|
+
.argument('<skill>', 'Skill identifier (e.g., @publisher/skill)')
|
|
116
|
+
.option('-v, --verbose', 'Show detailed verification info')
|
|
117
|
+
.option('--json', 'Output as JSON (for scripting/CI)')
|
|
118
|
+
.option('--offline', 'Skip DNS verification (offline mode)')
|
|
119
|
+
.action(async (skill, options) => {
|
|
120
|
+
try {
|
|
121
|
+
const result = await (0, index_1.checkSkill)(skill, {
|
|
122
|
+
skipDnsVerification: options.offline,
|
|
123
|
+
});
|
|
124
|
+
if (options.json) {
|
|
125
|
+
console.log(JSON.stringify(result, null, 2));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const risk = RISK_DISPLAY[result.risk];
|
|
129
|
+
console.log(`\n${risk.color()}${risk.symbol} ${result.risk.toUpperCase()} RISK${RESET()}\n`);
|
|
130
|
+
// Publisher info
|
|
131
|
+
console.log(`Publisher: @${result.publisher.name}`);
|
|
132
|
+
if (result.publisher.verified) {
|
|
133
|
+
console.log(`├─ ✅ Verified via DNS`);
|
|
134
|
+
if (result.publisher.domain) {
|
|
135
|
+
console.log(`├─ 🌐 Domain: ${result.publisher.domain}`);
|
|
136
|
+
}
|
|
137
|
+
if (result.publisher.verifiedAt && options.verbose) {
|
|
138
|
+
console.log(`└─ 📅 Verified at: ${result.publisher.verifiedAt.toISOString()}`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
console.log(`└─ Method: DNS TXT record`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
console.log(`├─ ❌ Not verified`);
|
|
146
|
+
if (result.publisher.failureReason && options.verbose) {
|
|
147
|
+
console.log(`└─ Reason: ${result.publisher.failureReason}`);
|
|
148
|
+
}
|
|
149
|
+
else if (options.offline) {
|
|
150
|
+
console.log(`└─ (DNS verification skipped - offline mode)`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
console.log(`└─ No valid DNS TXT record found`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
console.log();
|
|
157
|
+
// Permissions
|
|
158
|
+
console.log('Permissions:');
|
|
159
|
+
if (result.permissions.requested.length === 0) {
|
|
160
|
+
console.log('└─ None declared');
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
for (const perm of result.permissions.safe) {
|
|
164
|
+
console.log(`├─ ✅ ${perm}`);
|
|
165
|
+
}
|
|
166
|
+
for (const perm of result.permissions.reviewNeeded) {
|
|
167
|
+
console.log(`├─ ⚠️ ${perm} (review needed)`);
|
|
168
|
+
}
|
|
169
|
+
for (const perm of result.permissions.dangerous) {
|
|
170
|
+
console.log(`├─ ❌ ${perm} (DANGEROUS)`);
|
|
171
|
+
}
|
|
172
|
+
console.log(`└─ Risk score: ${result.permissions.riskScore}/100`);
|
|
173
|
+
}
|
|
174
|
+
console.log();
|
|
175
|
+
// Revocation
|
|
176
|
+
console.log('Revocation:');
|
|
177
|
+
if (result.revocation.revoked) {
|
|
178
|
+
console.log(`└─ 🚨 REVOKED: ${result.revocation.reason}`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
console.log(`└─ ✅ Not on blocklist`);
|
|
182
|
+
}
|
|
183
|
+
console.log();
|
|
184
|
+
// Verbose details
|
|
185
|
+
if (options.verbose) {
|
|
186
|
+
console.log('Details:');
|
|
187
|
+
console.log(`└─ Checked at: ${result.revocation.checkedAt.toISOString()}`);
|
|
188
|
+
}
|
|
189
|
+
// Exit with non-zero for high/critical risk
|
|
190
|
+
if (result.risk === 'critical' || result.risk === 'high') {
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
// Severity colors and symbols for secure command
|
|
200
|
+
const SEVERITY_DISPLAY = {
|
|
201
|
+
critical: { symbol: '🔴', color: () => colors.brightRed },
|
|
202
|
+
high: { symbol: '🟠', color: () => colors.red },
|
|
203
|
+
medium: { symbol: '🟡', color: () => colors.yellow },
|
|
204
|
+
low: { symbol: '🟢', color: () => colors.green },
|
|
205
|
+
};
|
|
206
|
+
function groupFindingsBySeverity(findings) {
|
|
207
|
+
const grouped = {
|
|
208
|
+
critical: [],
|
|
209
|
+
high: [],
|
|
210
|
+
medium: [],
|
|
211
|
+
low: [],
|
|
212
|
+
};
|
|
213
|
+
for (const finding of findings) {
|
|
214
|
+
grouped[finding.severity].push(finding);
|
|
215
|
+
}
|
|
216
|
+
return grouped;
|
|
217
|
+
}
|
|
218
|
+
function generateBenchmarkReport(findings, level, categoryFilter) {
|
|
219
|
+
// Get controls for the specified level
|
|
220
|
+
let controls = (0, index_1.getControlsForLevel)(level);
|
|
221
|
+
// Filter by category if specified
|
|
222
|
+
if (categoryFilter) {
|
|
223
|
+
const categoryControls = (0, index_1.getControlsForCategory)(categoryFilter);
|
|
224
|
+
if (categoryControls.length === 0) {
|
|
225
|
+
console.error(`Error: Unknown category '${categoryFilter}'.`);
|
|
226
|
+
console.error(`Available categories: ${index_1.OASB_1_CATEGORIES.map((c) => c.name).join(', ')}`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
controls = controls.filter((c) => c.category.toLowerCase() === categoryFilter.toLowerCase());
|
|
230
|
+
}
|
|
231
|
+
// Build a map of checkId -> finding for quick lookup
|
|
232
|
+
const findingsByCheckId = new Map();
|
|
233
|
+
for (const finding of findings) {
|
|
234
|
+
findingsByCheckId.set(finding.checkId, finding);
|
|
235
|
+
}
|
|
236
|
+
// Evaluate each control
|
|
237
|
+
const controlResults = [];
|
|
238
|
+
let l1Passed = 0, l1Total = 0;
|
|
239
|
+
let l2Passed = 0, l2Total = 0;
|
|
240
|
+
let l3Passed = 0, l3Total = 0;
|
|
241
|
+
let passedCount = 0, failedCount = 0, unverifiedCount = 0;
|
|
242
|
+
for (const control of controls) {
|
|
243
|
+
let status;
|
|
244
|
+
const relatedFindings = [];
|
|
245
|
+
let remediation;
|
|
246
|
+
if (control.verification === 'manual' || control.verification === 'forward') {
|
|
247
|
+
// Manual/forward controls are unverified (human must check)
|
|
248
|
+
status = 'unverified';
|
|
249
|
+
unverifiedCount++;
|
|
250
|
+
// Use control's remediation for manual/forward controls
|
|
251
|
+
remediation = control.remediation;
|
|
252
|
+
}
|
|
253
|
+
else if (control.checkIds.length === 0) {
|
|
254
|
+
// No automated checks defined
|
|
255
|
+
status = 'unverified';
|
|
256
|
+
unverifiedCount++;
|
|
257
|
+
remediation = control.remediation;
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
// Check all mapped check IDs
|
|
261
|
+
let hasAnyFinding = false;
|
|
262
|
+
let hasFailure = false;
|
|
263
|
+
for (const checkId of control.checkIds) {
|
|
264
|
+
const finding = findingsByCheckId.get(checkId);
|
|
265
|
+
if (finding) {
|
|
266
|
+
hasAnyFinding = true;
|
|
267
|
+
if (!finding.passed) {
|
|
268
|
+
hasFailure = true;
|
|
269
|
+
relatedFindings.push(`${checkId}: ${finding.description}`);
|
|
270
|
+
if (finding.fix) {
|
|
271
|
+
remediation = remediation || finding.fix;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Only mark as passed if we actually verified something
|
|
277
|
+
// Missing findings = unverified (not passed)
|
|
278
|
+
if (!hasAnyFinding) {
|
|
279
|
+
status = 'unverified';
|
|
280
|
+
unverifiedCount++;
|
|
281
|
+
remediation = control.remediation;
|
|
282
|
+
}
|
|
283
|
+
else if (hasFailure) {
|
|
284
|
+
status = 'failed';
|
|
285
|
+
failedCount++;
|
|
286
|
+
remediation = remediation || control.remediation;
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
status = 'passed';
|
|
290
|
+
passedCount++;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Count by level for compliance calculation
|
|
294
|
+
if (control.scored && status !== 'unverified') {
|
|
295
|
+
if (control.level === 'L1') {
|
|
296
|
+
l1Total++;
|
|
297
|
+
if (status === 'passed')
|
|
298
|
+
l1Passed++;
|
|
299
|
+
}
|
|
300
|
+
else if (control.level === 'L2') {
|
|
301
|
+
l2Total++;
|
|
302
|
+
if (status === 'passed')
|
|
303
|
+
l2Passed++;
|
|
304
|
+
}
|
|
305
|
+
else if (control.level === 'L3') {
|
|
306
|
+
l3Total++;
|
|
307
|
+
if (status === 'passed')
|
|
308
|
+
l3Passed++;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
controlResults.push({ control, status, findings: relatedFindings, remediation });
|
|
312
|
+
}
|
|
313
|
+
// Calculate compliance percentages
|
|
314
|
+
const l1Compliance = l1Total > 0 ? Math.round((l1Passed / l1Total) * 100) : 100;
|
|
315
|
+
const l2Compliance = l2Total > 0 ? Math.round((l2Passed / l2Total) * 100) : 100;
|
|
316
|
+
const l3Compliance = l3Total > 0 ? Math.round((l3Passed / l3Total) * 100) : 100;
|
|
317
|
+
const totalScored = l1Total + l2Total + l3Total;
|
|
318
|
+
const totalPassed = l1Passed + l2Passed + l3Passed;
|
|
319
|
+
const overallCompliance = totalScored > 0 ? Math.round((totalPassed / totalScored) * 100) : 100;
|
|
320
|
+
// Group results by category
|
|
321
|
+
const categoryResults = [];
|
|
322
|
+
for (const category of index_1.OASB_1_CATEGORIES) {
|
|
323
|
+
if (categoryFilter && category.name.toLowerCase() !== categoryFilter.toLowerCase())
|
|
324
|
+
continue;
|
|
325
|
+
const catControls = controlResults.filter((r) => r.control.category === category.name);
|
|
326
|
+
if (catControls.length === 0)
|
|
327
|
+
continue;
|
|
328
|
+
const passed = catControls.filter((r) => r.status === 'passed').length;
|
|
329
|
+
const failed = catControls.filter((r) => r.status === 'failed').length;
|
|
330
|
+
const unverified = catControls.filter((r) => r.status === 'unverified').length;
|
|
331
|
+
const compliance = (passed + failed) > 0 ? Math.round((passed / (passed + failed)) * 100) : 100;
|
|
332
|
+
categoryResults.push({
|
|
333
|
+
category: category.name,
|
|
334
|
+
compliance,
|
|
335
|
+
passed,
|
|
336
|
+
failed,
|
|
337
|
+
unverified,
|
|
338
|
+
controls: catControls.map((r) => ({
|
|
339
|
+
controlId: r.control.id,
|
|
340
|
+
name: r.control.name,
|
|
341
|
+
level: r.control.level,
|
|
342
|
+
status: r.status,
|
|
343
|
+
findings: r.findings,
|
|
344
|
+
remediation: r.remediation,
|
|
345
|
+
})),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
const rating = (0, index_1.calculateRating)(l1Compliance, l2Compliance, l3Compliance, level);
|
|
349
|
+
return {
|
|
350
|
+
benchmark: index_1.OASB_1_NAME,
|
|
351
|
+
version: index_1.OASB_1_VERSION,
|
|
352
|
+
level,
|
|
353
|
+
timestamp: new Date(),
|
|
354
|
+
compliance: overallCompliance,
|
|
355
|
+
l1Compliance,
|
|
356
|
+
l2Compliance,
|
|
357
|
+
l3Compliance,
|
|
358
|
+
rating,
|
|
359
|
+
categories: categoryResults,
|
|
360
|
+
totalControls: controls.length,
|
|
361
|
+
passedControls: passedCount,
|
|
362
|
+
failedControls: failedCount,
|
|
363
|
+
unverifiedControls: unverifiedCount,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
// SARIF 2.1.0 output for GitHub Security tab and IDE integration
|
|
367
|
+
function generateSarifOutput(benchmarkResult, findings, targetDir) {
|
|
368
|
+
const rules = [];
|
|
369
|
+
const results = [];
|
|
370
|
+
// Build rules and results from benchmark controls
|
|
371
|
+
for (const cat of benchmarkResult.categories) {
|
|
372
|
+
for (const ctrl of cat.controls) {
|
|
373
|
+
if (ctrl.status === 'failed') {
|
|
374
|
+
const ruleId = `OASB-1/${ctrl.controlId}`;
|
|
375
|
+
const severityScore = ctrl.level === 'L1' ? '8.0' : ctrl.level === 'L2' ? '6.0' : '4.0';
|
|
376
|
+
const sarifLevel = ctrl.level === 'L1' ? 'error' : ctrl.level === 'L2' ? 'warning' : 'note';
|
|
377
|
+
rules.push({
|
|
378
|
+
id: ruleId,
|
|
379
|
+
name: ctrl.name.replace(/\s+/g, ''),
|
|
380
|
+
shortDescription: { text: ctrl.name },
|
|
381
|
+
fullDescription: { text: `OASB-1 ${ctrl.level} Control: ${ctrl.name}` },
|
|
382
|
+
help: {
|
|
383
|
+
text: ctrl.remediation || `Fix the ${ctrl.name} control to achieve compliance.`,
|
|
384
|
+
markdown: ctrl.remediation ? `**Remediation:** ${ctrl.remediation}` : undefined,
|
|
385
|
+
},
|
|
386
|
+
helpUri: `https://oasb.ai/controls/${ctrl.controlId}`,
|
|
387
|
+
defaultConfiguration: { level: sarifLevel },
|
|
388
|
+
properties: {
|
|
389
|
+
'security-severity': severityScore,
|
|
390
|
+
tags: ['security', 'oasb-1', ctrl.level.toLowerCase()],
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
// Find related findings for locations
|
|
394
|
+
const relatedFindings = findings.filter(f => ctrl.findings.some(cf => cf.includes(f.checkId)));
|
|
395
|
+
if (relatedFindings.length > 0) {
|
|
396
|
+
for (const finding of relatedFindings) {
|
|
397
|
+
results.push({
|
|
398
|
+
ruleId,
|
|
399
|
+
level: sarifLevel,
|
|
400
|
+
message: { text: finding.description },
|
|
401
|
+
locations: finding.file ? [{
|
|
402
|
+
physicalLocation: {
|
|
403
|
+
artifactLocation: { uri: finding.file.replace(targetDir + '/', '') },
|
|
404
|
+
region: finding.line ? { startLine: finding.line } : undefined,
|
|
405
|
+
},
|
|
406
|
+
}] : undefined,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
// No specific location, just report the control failure
|
|
412
|
+
results.push({
|
|
413
|
+
ruleId,
|
|
414
|
+
level: sarifLevel,
|
|
415
|
+
message: { text: ctrl.findings.join('; ') || `Control ${ctrl.controlId} failed` },
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const sarif = {
|
|
422
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
423
|
+
version: '2.1.0',
|
|
424
|
+
runs: [{
|
|
425
|
+
tool: {
|
|
426
|
+
driver: {
|
|
427
|
+
name: 'HackMyAgent',
|
|
428
|
+
version: index_1.VERSION,
|
|
429
|
+
informationUri: 'https://hackmyagent.com',
|
|
430
|
+
rules,
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
results,
|
|
434
|
+
}],
|
|
435
|
+
};
|
|
436
|
+
return JSON.stringify(sarif, null, 2);
|
|
437
|
+
}
|
|
438
|
+
// HTML report for shareable compliance documentation
|
|
439
|
+
function generateHtmlReport(result) {
|
|
440
|
+
const ratingColor = {
|
|
441
|
+
'Certified': '#22c55e',
|
|
442
|
+
'Compliant': '#22c55e',
|
|
443
|
+
'Passing': '#eab308',
|
|
444
|
+
'Needs Improvement': '#f97316',
|
|
445
|
+
'Failing': '#ef4444',
|
|
446
|
+
}[result.rating] || '#94a3b8';
|
|
447
|
+
const ratingBg = {
|
|
448
|
+
'Certified': 'rgba(34, 197, 94, 0.15)',
|
|
449
|
+
'Compliant': 'rgba(34, 197, 94, 0.15)',
|
|
450
|
+
'Passing': 'rgba(234, 179, 8, 0.15)',
|
|
451
|
+
'Needs Improvement': 'rgba(249, 115, 22, 0.15)',
|
|
452
|
+
'Failing': 'rgba(239, 68, 68, 0.15)',
|
|
453
|
+
}[result.rating] || 'rgba(148, 163, 184, 0.15)';
|
|
454
|
+
// Generate donut chart SVG
|
|
455
|
+
const donutRadius = 70;
|
|
456
|
+
const donutStroke = 14;
|
|
457
|
+
const donutCircumference = 2 * Math.PI * donutRadius;
|
|
458
|
+
const donutOffset = donutCircumference * (1 - result.compliance / 100);
|
|
459
|
+
const complianceColor = result.compliance >= 90 ? '#22c55e' : result.compliance >= 70 ? '#eab308' : '#ef4444';
|
|
460
|
+
// Generate radar chart data points
|
|
461
|
+
const radarCategories = result.categories.slice(0, 10); // Max 10 for radar
|
|
462
|
+
const radarPoints = [];
|
|
463
|
+
const radarLabels = [];
|
|
464
|
+
const radarCenter = 120;
|
|
465
|
+
const radarRadius = 90;
|
|
466
|
+
// Category name abbreviations for radar chart labels
|
|
467
|
+
const categoryAbbreviations = {
|
|
468
|
+
'Identity & Provenance': 'Identity',
|
|
469
|
+
'Capability & Authorization': 'Capability',
|
|
470
|
+
'Input Security': 'Input',
|
|
471
|
+
'Output Security': 'Output',
|
|
472
|
+
'Credential Protection': 'Credentials',
|
|
473
|
+
'Supply Chain Integrity': 'Supply Chain',
|
|
474
|
+
'Agent-to-Agent Security': 'A2A Security',
|
|
475
|
+
'Memory & Context Integrity': 'Memory',
|
|
476
|
+
'Operational Security': 'Operations',
|
|
477
|
+
'Monitoring & Response': 'Monitoring',
|
|
478
|
+
};
|
|
479
|
+
radarCategories.forEach((cat, i) => {
|
|
480
|
+
const angle = (Math.PI * 2 * i) / radarCategories.length - Math.PI / 2;
|
|
481
|
+
// Use minimum 5% so 0% categories still show on the chart edge (not at center)
|
|
482
|
+
const value = Math.max(0.05, cat.compliance / 100);
|
|
483
|
+
const x = radarCenter + Math.cos(angle) * radarRadius * value;
|
|
484
|
+
const y = radarCenter + Math.sin(angle) * radarRadius * value;
|
|
485
|
+
radarPoints.push(`${x},${y}`);
|
|
486
|
+
// Label position (slightly outside)
|
|
487
|
+
const labelX = radarCenter + Math.cos(angle) * (radarRadius + 20);
|
|
488
|
+
const labelY = radarCenter + Math.sin(angle) * (radarRadius + 20);
|
|
489
|
+
const shortName = categoryAbbreviations[cat.category] || cat.category.split(' ')[0];
|
|
490
|
+
radarLabels.push(`<text x="${labelX}" y="${labelY}" text-anchor="middle" dominant-baseline="middle" fill="#94a3b8" font-size="10" font-weight="500">${escapeHtml(shortName)}</text>`);
|
|
491
|
+
});
|
|
492
|
+
// Generate radar grid lines
|
|
493
|
+
const radarGrid = [0.25, 0.5, 0.75, 1].map(scale => {
|
|
494
|
+
const points = radarCategories.map((_, i) => {
|
|
495
|
+
const angle = (Math.PI * 2 * i) / radarCategories.length - Math.PI / 2;
|
|
496
|
+
const x = radarCenter + Math.cos(angle) * radarRadius * scale;
|
|
497
|
+
const y = radarCenter + Math.sin(angle) * radarRadius * scale;
|
|
498
|
+
return `${x},${y}`;
|
|
499
|
+
}).join(' ');
|
|
500
|
+
return `<polygon points="${points}" fill="none" stroke="#334155" stroke-width="1"/>`;
|
|
501
|
+
}).join('');
|
|
502
|
+
// Radar axis lines
|
|
503
|
+
const radarAxes = radarCategories.map((_, i) => {
|
|
504
|
+
const angle = (Math.PI * 2 * i) / radarCategories.length - Math.PI / 2;
|
|
505
|
+
const x = radarCenter + Math.cos(angle) * radarRadius;
|
|
506
|
+
const y = radarCenter + Math.sin(angle) * radarRadius;
|
|
507
|
+
return `<line x1="${radarCenter}" y1="${radarCenter}" x2="${x}" y2="${y}" stroke="#334155" stroke-width="1"/>`;
|
|
508
|
+
}).join('');
|
|
509
|
+
// Collect all controls for statistics
|
|
510
|
+
const allControls = result.categories.flatMap(cat => cat.controls);
|
|
511
|
+
const failedControls = allControls.filter(ctrl => ctrl.status === 'failed');
|
|
512
|
+
const passedControls = allControls.filter(ctrl => ctrl.status === 'passed');
|
|
513
|
+
const unverifiedControls = allControls.filter(ctrl => ctrl.status === 'unverified');
|
|
514
|
+
// Level breakdown stats
|
|
515
|
+
const levelStats = {
|
|
516
|
+
L1: { passed: 0, failed: 0, total: 0 },
|
|
517
|
+
L2: { passed: 0, failed: 0, total: 0 },
|
|
518
|
+
L3: { passed: 0, failed: 0, total: 0 },
|
|
519
|
+
};
|
|
520
|
+
allControls.forEach(ctrl => {
|
|
521
|
+
const lvl = ctrl.level;
|
|
522
|
+
if (levelStats[lvl]) {
|
|
523
|
+
levelStats[lvl].total++;
|
|
524
|
+
if (ctrl.status === 'passed')
|
|
525
|
+
levelStats[lvl].passed++;
|
|
526
|
+
if (ctrl.status === 'failed')
|
|
527
|
+
levelStats[lvl].failed++;
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
// Find worst category
|
|
531
|
+
const worstCategory = result.categories
|
|
532
|
+
.filter(cat => cat.passed + cat.failed > 0)
|
|
533
|
+
.sort((a, b) => a.compliance - b.compliance)[0];
|
|
534
|
+
// Security grade based on compliance
|
|
535
|
+
const getGrade = (pct) => {
|
|
536
|
+
if (pct >= 95)
|
|
537
|
+
return { letter: 'A+', color: '#22c55e' };
|
|
538
|
+
if (pct >= 90)
|
|
539
|
+
return { letter: 'A', color: '#22c55e' };
|
|
540
|
+
if (pct >= 85)
|
|
541
|
+
return { letter: 'B+', color: '#84cc16' };
|
|
542
|
+
if (pct >= 80)
|
|
543
|
+
return { letter: 'B', color: '#84cc16' };
|
|
544
|
+
if (pct >= 75)
|
|
545
|
+
return { letter: 'C+', color: '#eab308' };
|
|
546
|
+
if (pct >= 70)
|
|
547
|
+
return { letter: 'C', color: '#eab308' };
|
|
548
|
+
if (pct >= 60)
|
|
549
|
+
return { letter: 'D', color: '#f97316' };
|
|
550
|
+
return { letter: 'F', color: '#ef4444' };
|
|
551
|
+
};
|
|
552
|
+
const grade = getGrade(result.compliance);
|
|
553
|
+
// Generate executive summary items
|
|
554
|
+
const executiveSummary = failedControls.length === 0
|
|
555
|
+
? '<div class="exec-item success"><span class="exec-icon">✓</span><span>All controls passing at this level</span></div>'
|
|
556
|
+
: failedControls.slice(0, 5).map(ctrl => `<div class="exec-item critical"><span class="exec-icon">!</span><span><strong>${ctrl.controlId}</strong>: ${escapeHtml(ctrl.name)}</span></div>`).join('') + (failedControls.length > 5 ? `<div class="exec-item warning"><span class="exec-icon">+</span><span>${failedControls.length - 5} more issues not shown</span></div>` : '');
|
|
557
|
+
// SVG icons for professional look (no emojis)
|
|
558
|
+
const icons = {
|
|
559
|
+
check: '<svg class="icon icon-check" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>',
|
|
560
|
+
x: '<svg class="icon icon-x" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>',
|
|
561
|
+
warning: '<svg class="icon icon-warning" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>',
|
|
562
|
+
circle: '<svg class="icon icon-circle" viewBox="0 0 20 20" fill="currentColor"><circle cx="10" cy="10" r="4"/></svg>',
|
|
563
|
+
shield: '<svg class="icon icon-shield" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clip-rule="evenodd"/></svg>',
|
|
564
|
+
print: '<svg class="icon icon-print" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5 4v3H4a2 2 0 00-2 2v3a2 2 0 002 2h1v2a2 2 0 002 2h6a2 2 0 002-2v-2h1a2 2 0 002-2V9a2 2 0 00-2-2h-1V4a2 2 0 00-2-2H7a2 2 0 00-2 2zm8 0H7v3h6V4zm0 8H7v4h6v-4z" clip-rule="evenodd"/></svg>',
|
|
565
|
+
};
|
|
566
|
+
// Category rows with collapsible sections
|
|
567
|
+
const categoryRows = result.categories.map((cat, catIndex) => {
|
|
568
|
+
const statusIcon = cat.failed === 0 ? icons.check : cat.passed > 0 ? icons.warning : icons.x;
|
|
569
|
+
const statusClass = cat.failed === 0 ? 'status-pass' : cat.passed > 0 ? 'status-warn' : 'status-fail';
|
|
570
|
+
const barColor = cat.compliance >= 90 ? '#22c55e' : cat.compliance >= 70 ? '#eab308' : '#ef4444';
|
|
571
|
+
const controlRows = cat.controls.map(ctrl => {
|
|
572
|
+
const statusSvg = ctrl.status === 'passed' ? icons.check : ctrl.status === 'failed' ? icons.x : icons.circle;
|
|
573
|
+
const ctrlStatusClass = ctrl.status === 'passed' ? 'status-pass' : ctrl.status === 'failed' ? 'status-fail' : 'status-unverified';
|
|
574
|
+
const findingsList = ctrl.findings.length > 0
|
|
575
|
+
? `<ul class="findings">${ctrl.findings.map(f => `<li>${escapeHtml(f)}</li>`).join('')}</ul>`
|
|
576
|
+
: '';
|
|
577
|
+
const remediation = ctrl.remediation
|
|
578
|
+
? `<div class="remediation"><strong>Remediation:</strong> ${escapeHtml(ctrl.remediation)}</div>`
|
|
579
|
+
: '';
|
|
580
|
+
return `
|
|
581
|
+
<tr class="control-row ${ctrl.status}">
|
|
582
|
+
<td class="status-cell"><span class="${ctrlStatusClass}">${statusSvg}</span></td>
|
|
583
|
+
<td class="id-cell"><code>${ctrl.controlId}</code></td>
|
|
584
|
+
<td class="name-cell">${escapeHtml(ctrl.name)}</td>
|
|
585
|
+
<td class="level-cell"><span class="level-badge level-${ctrl.level.toLowerCase()}">${ctrl.level}</span></td>
|
|
586
|
+
<td class="details-cell">${findingsList}${remediation}</td>
|
|
587
|
+
</tr>`;
|
|
588
|
+
}).join('');
|
|
589
|
+
return `
|
|
590
|
+
<div class="category" id="cat-${catIndex}">
|
|
591
|
+
<div class="category-header" onclick="toggleCategory(${catIndex})">
|
|
592
|
+
<span class="category-icon ${statusClass}">${statusIcon}</span>
|
|
593
|
+
<span class="category-name">${escapeHtml(cat.category)}</span>
|
|
594
|
+
<div class="category-meta">
|
|
595
|
+
<span class="category-score">${cat.passed}/${cat.passed + cat.failed}</span>
|
|
596
|
+
<div class="mini-bar"><div class="mini-fill" style="width: ${cat.compliance}%; background: ${barColor};"></div></div>
|
|
597
|
+
<span class="category-percent">${cat.compliance}%</span>
|
|
598
|
+
<span class="chevron">▼</span>
|
|
599
|
+
</div>
|
|
600
|
+
</div>
|
|
601
|
+
<div class="category-content">
|
|
602
|
+
<table class="controls-table">
|
|
603
|
+
<thead><tr><th></th><th>Control ID</th><th>Control Name</th><th>Level</th><th>Details</th></tr></thead>
|
|
604
|
+
<tbody>${controlRows}</tbody>
|
|
605
|
+
</table>
|
|
606
|
+
</div>
|
|
607
|
+
</div>`;
|
|
608
|
+
}).join('');
|
|
609
|
+
// Level description
|
|
610
|
+
const levelDesc = {
|
|
611
|
+
'L1': 'Essential baseline security every agent should implement',
|
|
612
|
+
'L2': 'Defense-in-depth for production systems',
|
|
613
|
+
'L3': 'Maximum security for high-risk or regulated environments'
|
|
614
|
+
}[result.level] || '';
|
|
615
|
+
return `<!DOCTYPE html>
|
|
616
|
+
<html lang="en">
|
|
617
|
+
<head>
|
|
618
|
+
<meta charset="UTF-8">
|
|
619
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
620
|
+
<title>OASB-1 Compliance Report | ${result.rating}</title>
|
|
621
|
+
<style>
|
|
622
|
+
:root {
|
|
623
|
+
--bg-primary: #0a0f1a;
|
|
624
|
+
--bg-secondary: #111827;
|
|
625
|
+
--bg-tertiary: #1f2937;
|
|
626
|
+
--text-primary: #f1f5f9;
|
|
627
|
+
--text-secondary: #94a3b8;
|
|
628
|
+
--text-muted: #64748b;
|
|
629
|
+
--border: #334155;
|
|
630
|
+
--accent: #3b82f6;
|
|
631
|
+
--success: #22c55e;
|
|
632
|
+
--warning: #eab308;
|
|
633
|
+
--danger: #ef4444;
|
|
634
|
+
}
|
|
635
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
636
|
+
body {
|
|
637
|
+
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
638
|
+
background: var(--bg-primary);
|
|
639
|
+
color: var(--text-primary);
|
|
640
|
+
line-height: 1.6;
|
|
641
|
+
padding: 2rem;
|
|
642
|
+
font-size: 14px;
|
|
643
|
+
}
|
|
644
|
+
.container { max-width: 1400px; margin: 0 auto; }
|
|
645
|
+
|
|
646
|
+
/* Header */
|
|
647
|
+
.header {
|
|
648
|
+
display: flex;
|
|
649
|
+
justify-content: space-between;
|
|
650
|
+
align-items: center;
|
|
651
|
+
margin-bottom: 2rem;
|
|
652
|
+
padding: 1.5rem 2rem;
|
|
653
|
+
background: var(--bg-secondary);
|
|
654
|
+
border-radius: 12px;
|
|
655
|
+
border: 1px solid var(--border);
|
|
656
|
+
}
|
|
657
|
+
.header-left h1 {
|
|
658
|
+
font-size: 1.5rem;
|
|
659
|
+
font-weight: 700;
|
|
660
|
+
display: flex;
|
|
661
|
+
align-items: center;
|
|
662
|
+
gap: 0.75rem;
|
|
663
|
+
}
|
|
664
|
+
.header-left .meta { color: var(--text-muted); font-size: 0.8rem; margin-top: 0.25rem; }
|
|
665
|
+
.header-icon { display: inline-flex; margin-right: 0.5rem; }
|
|
666
|
+
.header-icon .icon { width: 24px; height: 24px; color: var(--accent); }
|
|
667
|
+
.header-right { display: flex; align-items: center; gap: 1rem; }
|
|
668
|
+
.rating-badge {
|
|
669
|
+
display: inline-block;
|
|
670
|
+
padding: 0.375rem 1rem;
|
|
671
|
+
border-radius: 6px;
|
|
672
|
+
font-weight: 600;
|
|
673
|
+
font-size: 0.875rem;
|
|
674
|
+
background: ${ratingBg};
|
|
675
|
+
color: ${ratingColor};
|
|
676
|
+
border: 1px solid ${ratingColor}40;
|
|
677
|
+
}
|
|
678
|
+
.level-tag {
|
|
679
|
+
display: inline-block;
|
|
680
|
+
padding: 0.375rem 1rem;
|
|
681
|
+
background: var(--accent);
|
|
682
|
+
color: white;
|
|
683
|
+
border-radius: 6px;
|
|
684
|
+
font-size: 0.875rem;
|
|
685
|
+
font-weight: 600;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/* SVG Icons */
|
|
689
|
+
.icon { width: 16px; height: 16px; display: inline-block; vertical-align: middle; }
|
|
690
|
+
.status-pass { color: var(--success); }
|
|
691
|
+
.status-fail { color: var(--danger); }
|
|
692
|
+
.status-warn { color: var(--warning); }
|
|
693
|
+
.status-unverified { color: var(--text-muted); }
|
|
694
|
+
.category-icon { display: flex; align-items: center; }
|
|
695
|
+
.category-icon .icon { width: 18px; height: 18px; }
|
|
696
|
+
.footer-btn .icon { width: 14px; height: 14px; margin-right: 0.375rem; }
|
|
697
|
+
|
|
698
|
+
/* Dashboard grid */
|
|
699
|
+
.dashboard {
|
|
700
|
+
display: grid;
|
|
701
|
+
grid-template-columns: 280px 1fr 300px;
|
|
702
|
+
gap: 1.5rem;
|
|
703
|
+
margin-bottom: 2rem;
|
|
704
|
+
}
|
|
705
|
+
@media (max-width: 1200px) {
|
|
706
|
+
.dashboard { grid-template-columns: 1fr 1fr; }
|
|
707
|
+
.radar-section { grid-column: span 2; }
|
|
708
|
+
}
|
|
709
|
+
@media (max-width: 768px) {
|
|
710
|
+
.dashboard { grid-template-columns: 1fr; }
|
|
711
|
+
.radar-section { grid-column: span 1; }
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/* Score card - Prowler style */
|
|
715
|
+
.score-card {
|
|
716
|
+
background: var(--bg-secondary);
|
|
717
|
+
border-radius: 12px;
|
|
718
|
+
padding: 1.25rem;
|
|
719
|
+
border: 1px solid var(--border);
|
|
720
|
+
}
|
|
721
|
+
.score-header {
|
|
722
|
+
display: flex;
|
|
723
|
+
align-items: center;
|
|
724
|
+
gap: 1rem;
|
|
725
|
+
margin-bottom: 1.25rem;
|
|
726
|
+
padding-bottom: 1rem;
|
|
727
|
+
border-bottom: 1px solid var(--border);
|
|
728
|
+
}
|
|
729
|
+
.score-grade {
|
|
730
|
+
width: 56px;
|
|
731
|
+
height: 56px;
|
|
732
|
+
border-radius: 12px;
|
|
733
|
+
border: 2px solid;
|
|
734
|
+
display: flex;
|
|
735
|
+
align-items: center;
|
|
736
|
+
justify-content: center;
|
|
737
|
+
}
|
|
738
|
+
.grade-letter { font-size: 1.75rem; font-weight: 800; }
|
|
739
|
+
.score-main { flex: 1; }
|
|
740
|
+
.score-pct { font-size: 2rem; font-weight: 700; color: var(--text-primary); line-height: 1; }
|
|
741
|
+
.score-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-top: 0.25rem; }
|
|
742
|
+
|
|
743
|
+
.score-bars { margin-bottom: 1rem; }
|
|
744
|
+
.score-bar-row {
|
|
745
|
+
display: flex;
|
|
746
|
+
align-items: center;
|
|
747
|
+
gap: 0.75rem;
|
|
748
|
+
margin-bottom: 0.5rem;
|
|
749
|
+
}
|
|
750
|
+
.bar-label { width: 50px; font-size: 0.75rem; color: var(--text-secondary); }
|
|
751
|
+
.bar-track { flex: 1; height: 8px; background: var(--bg-tertiary); border-radius: 4px; overflow: hidden; }
|
|
752
|
+
.bar-fill { height: 100%; border-radius: 4px; transition: width 0.3s; }
|
|
753
|
+
.bar-pass { background: var(--success); }
|
|
754
|
+
.bar-fail { background: var(--danger); }
|
|
755
|
+
.bar-manual { background: var(--text-muted); }
|
|
756
|
+
.bar-count { width: 24px; font-size: 0.8rem; font-weight: 600; text-align: right; color: var(--text-primary); }
|
|
757
|
+
|
|
758
|
+
.level-breakdown {
|
|
759
|
+
display: flex;
|
|
760
|
+
gap: 0.75rem;
|
|
761
|
+
padding: 0.75rem;
|
|
762
|
+
background: var(--bg-tertiary);
|
|
763
|
+
border-radius: 8px;
|
|
764
|
+
margin-bottom: 1rem;
|
|
765
|
+
}
|
|
766
|
+
.level-row { display: flex; align-items: center; gap: 0.375rem; }
|
|
767
|
+
.level-stat { font-size: 0.8rem; color: var(--text-secondary); }
|
|
768
|
+
|
|
769
|
+
.worst-category {
|
|
770
|
+
display: flex;
|
|
771
|
+
align-items: center;
|
|
772
|
+
gap: 0.5rem;
|
|
773
|
+
padding: 0.625rem 0.75rem;
|
|
774
|
+
background: rgba(239, 68, 68, 0.1);
|
|
775
|
+
border-radius: 6px;
|
|
776
|
+
border-left: 3px solid var(--danger);
|
|
777
|
+
}
|
|
778
|
+
.worst-label { font-size: 0.7rem; color: var(--danger); text-transform: uppercase; font-weight: 600; }
|
|
779
|
+
.worst-name { flex: 1; font-size: 0.8rem; color: var(--text-primary); }
|
|
780
|
+
.worst-pct { font-size: 0.85rem; font-weight: 700; }
|
|
781
|
+
|
|
782
|
+
/* Radar chart */
|
|
783
|
+
.radar-section {
|
|
784
|
+
background: var(--bg-secondary);
|
|
785
|
+
border-radius: 12px;
|
|
786
|
+
padding: 1.5rem;
|
|
787
|
+
border: 1px solid var(--border);
|
|
788
|
+
}
|
|
789
|
+
.radar-section h3 {
|
|
790
|
+
font-size: 0.85rem;
|
|
791
|
+
color: var(--text-secondary);
|
|
792
|
+
text-transform: uppercase;
|
|
793
|
+
letter-spacing: 0.05em;
|
|
794
|
+
margin-bottom: 1rem;
|
|
795
|
+
}
|
|
796
|
+
.radar-container { display: flex; justify-content: center; }
|
|
797
|
+
|
|
798
|
+
/* Executive summary */
|
|
799
|
+
.exec-section {
|
|
800
|
+
background: var(--bg-secondary);
|
|
801
|
+
border-radius: 12px;
|
|
802
|
+
padding: 1.5rem;
|
|
803
|
+
border: 1px solid var(--border);
|
|
804
|
+
}
|
|
805
|
+
.exec-section h3 {
|
|
806
|
+
font-size: 0.85rem;
|
|
807
|
+
color: var(--text-secondary);
|
|
808
|
+
text-transform: uppercase;
|
|
809
|
+
letter-spacing: 0.05em;
|
|
810
|
+
margin-bottom: 1rem;
|
|
811
|
+
}
|
|
812
|
+
.exec-item {
|
|
813
|
+
display: flex;
|
|
814
|
+
align-items: flex-start;
|
|
815
|
+
gap: 0.75rem;
|
|
816
|
+
padding: 0.75rem;
|
|
817
|
+
margin-bottom: 0.5rem;
|
|
818
|
+
border-radius: 6px;
|
|
819
|
+
font-size: 0.85rem;
|
|
820
|
+
}
|
|
821
|
+
.exec-item.critical { background: rgba(239, 68, 68, 0.1); border-left: 3px solid var(--danger); }
|
|
822
|
+
.exec-item.warning { background: rgba(234, 179, 8, 0.1); border-left: 3px solid var(--warning); }
|
|
823
|
+
.exec-item.success { background: rgba(34, 197, 94, 0.1); border-left: 3px solid var(--success); }
|
|
824
|
+
.exec-icon {
|
|
825
|
+
width: 20px;
|
|
826
|
+
height: 20px;
|
|
827
|
+
border-radius: 50%;
|
|
828
|
+
display: flex;
|
|
829
|
+
align-items: center;
|
|
830
|
+
justify-content: center;
|
|
831
|
+
font-weight: 700;
|
|
832
|
+
font-size: 0.75rem;
|
|
833
|
+
flex-shrink: 0;
|
|
834
|
+
}
|
|
835
|
+
.exec-item.critical .exec-icon { background: var(--danger); color: white; }
|
|
836
|
+
.exec-item.warning .exec-icon { background: var(--warning); color: black; }
|
|
837
|
+
.exec-item.success .exec-icon { background: var(--success); color: white; }
|
|
838
|
+
|
|
839
|
+
/* Categories */
|
|
840
|
+
.categories-header {
|
|
841
|
+
display: flex;
|
|
842
|
+
justify-content: space-between;
|
|
843
|
+
align-items: center;
|
|
844
|
+
margin-bottom: 1rem;
|
|
845
|
+
}
|
|
846
|
+
.categories-header h2 { font-size: 1.1rem; }
|
|
847
|
+
.expand-all {
|
|
848
|
+
background: var(--bg-tertiary);
|
|
849
|
+
border: 1px solid var(--border);
|
|
850
|
+
color: var(--text-secondary);
|
|
851
|
+
padding: 0.5rem 1rem;
|
|
852
|
+
border-radius: 6px;
|
|
853
|
+
cursor: pointer;
|
|
854
|
+
font-size: 0.8rem;
|
|
855
|
+
}
|
|
856
|
+
.expand-all:hover { background: var(--border); }
|
|
857
|
+
|
|
858
|
+
.category {
|
|
859
|
+
background: var(--bg-secondary);
|
|
860
|
+
border-radius: 8px;
|
|
861
|
+
margin-bottom: 0.75rem;
|
|
862
|
+
border: 1px solid var(--border);
|
|
863
|
+
overflow: hidden;
|
|
864
|
+
}
|
|
865
|
+
.category-header {
|
|
866
|
+
display: flex;
|
|
867
|
+
align-items: center;
|
|
868
|
+
gap: 0.75rem;
|
|
869
|
+
padding: 1rem 1.25rem;
|
|
870
|
+
cursor: pointer;
|
|
871
|
+
transition: background 0.15s;
|
|
872
|
+
}
|
|
873
|
+
.category-header:hover { background: var(--bg-tertiary); }
|
|
874
|
+
.category-icon { font-size: 1.1rem; }
|
|
875
|
+
.category-name { flex: 1; font-weight: 500; }
|
|
876
|
+
.category-meta { display: flex; align-items: center; gap: 0.75rem; }
|
|
877
|
+
.category-score { color: var(--text-secondary); font-size: 0.85rem; font-weight: 500; }
|
|
878
|
+
.mini-bar { width: 60px; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
|
|
879
|
+
.mini-fill { height: 100%; border-radius: 3px; }
|
|
880
|
+
.category-percent { color: var(--text-muted); font-size: 0.85rem; width: 40px; text-align: right; }
|
|
881
|
+
.chevron {
|
|
882
|
+
color: var(--text-muted);
|
|
883
|
+
font-size: 0.7rem;
|
|
884
|
+
transition: transform 0.2s;
|
|
885
|
+
margin-left: 0.5rem;
|
|
886
|
+
}
|
|
887
|
+
.category.collapsed .chevron { transform: rotate(-90deg); }
|
|
888
|
+
.category.collapsed .category-content { display: none; }
|
|
889
|
+
|
|
890
|
+
.category-content { border-top: 1px solid var(--border); }
|
|
891
|
+
.controls-table { width: 100%; border-collapse: collapse; }
|
|
892
|
+
.controls-table th {
|
|
893
|
+
padding: 0.75rem 1rem;
|
|
894
|
+
text-align: left;
|
|
895
|
+
background: var(--bg-primary);
|
|
896
|
+
color: var(--text-muted);
|
|
897
|
+
font-weight: 500;
|
|
898
|
+
font-size: 0.75rem;
|
|
899
|
+
text-transform: uppercase;
|
|
900
|
+
letter-spacing: 0.03em;
|
|
901
|
+
}
|
|
902
|
+
.controls-table td {
|
|
903
|
+
padding: 0.875rem 1rem;
|
|
904
|
+
border-top: 1px solid var(--border);
|
|
905
|
+
vertical-align: top;
|
|
906
|
+
}
|
|
907
|
+
.status-cell { width: 40px; text-align: center; }
|
|
908
|
+
.id-cell { width: 100px; }
|
|
909
|
+
.id-cell code {
|
|
910
|
+
background: var(--bg-tertiary);
|
|
911
|
+
padding: 0.2rem 0.5rem;
|
|
912
|
+
border-radius: 4px;
|
|
913
|
+
font-size: 0.8rem;
|
|
914
|
+
color: var(--accent);
|
|
915
|
+
}
|
|
916
|
+
.name-cell { width: 30%; }
|
|
917
|
+
.level-cell { width: 60px; }
|
|
918
|
+
.details-cell { color: var(--text-secondary); font-size: 0.85rem; }
|
|
919
|
+
.control-row.failed { background: rgba(239, 68, 68, 0.05); }
|
|
920
|
+
.control-row.unverified { opacity: 0.5; }
|
|
921
|
+
|
|
922
|
+
.level-badge {
|
|
923
|
+
padding: 0.2rem 0.6rem;
|
|
924
|
+
border-radius: 4px;
|
|
925
|
+
font-size: 0.7rem;
|
|
926
|
+
font-weight: 600;
|
|
927
|
+
text-transform: uppercase;
|
|
928
|
+
}
|
|
929
|
+
.level-l1 { background: #7c3aed; color: white; }
|
|
930
|
+
.level-l2 { background: #2563eb; color: white; }
|
|
931
|
+
.level-l3 { background: #059669; color: white; }
|
|
932
|
+
|
|
933
|
+
.findings {
|
|
934
|
+
margin: 0.25rem 0 0.5rem;
|
|
935
|
+
padding-left: 1.25rem;
|
|
936
|
+
color: #f87171;
|
|
937
|
+
list-style-type: disc;
|
|
938
|
+
}
|
|
939
|
+
.findings li { margin-bottom: 0.25rem; }
|
|
940
|
+
.remediation {
|
|
941
|
+
margin-top: 0.5rem;
|
|
942
|
+
padding: 0.625rem 0.875rem;
|
|
943
|
+
background: var(--bg-tertiary);
|
|
944
|
+
border-radius: 6px;
|
|
945
|
+
font-size: 0.8rem;
|
|
946
|
+
border-left: 3px solid var(--accent);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/* Footer */
|
|
950
|
+
.footer {
|
|
951
|
+
display: flex;
|
|
952
|
+
justify-content: space-between;
|
|
953
|
+
align-items: center;
|
|
954
|
+
margin-top: 2rem;
|
|
955
|
+
padding: 1.5rem;
|
|
956
|
+
background: var(--bg-secondary);
|
|
957
|
+
border-radius: 12px;
|
|
958
|
+
border: 1px solid var(--border);
|
|
959
|
+
color: var(--text-muted);
|
|
960
|
+
font-size: 0.85rem;
|
|
961
|
+
}
|
|
962
|
+
.footer a { color: var(--accent); text-decoration: none; }
|
|
963
|
+
.footer a:hover { text-decoration: underline; }
|
|
964
|
+
.footer-actions { display: flex; gap: 1rem; }
|
|
965
|
+
.footer-btn {
|
|
966
|
+
padding: 0.5rem 1rem;
|
|
967
|
+
background: var(--bg-tertiary);
|
|
968
|
+
border: 1px solid var(--border);
|
|
969
|
+
border-radius: 6px;
|
|
970
|
+
color: var(--text-primary);
|
|
971
|
+
cursor: pointer;
|
|
972
|
+
font-size: 0.8rem;
|
|
973
|
+
}
|
|
974
|
+
.footer-btn:hover { background: var(--border); }
|
|
975
|
+
|
|
976
|
+
/* Print styles */
|
|
977
|
+
@media print {
|
|
978
|
+
body { background: white; color: black; padding: 1rem; }
|
|
979
|
+
.container { max-width: 100%; }
|
|
980
|
+
.header, .donut-card, .radar-section, .exec-section, .category, .footer {
|
|
981
|
+
background: white;
|
|
982
|
+
border: 1px solid #ddd;
|
|
983
|
+
break-inside: avoid;
|
|
984
|
+
}
|
|
985
|
+
.category.collapsed .category-content { display: block !important; }
|
|
986
|
+
.chevron, .expand-all, .footer-actions { display: none; }
|
|
987
|
+
.category-header { cursor: default; }
|
|
988
|
+
.control-row.failed { background: #fff0f0; }
|
|
989
|
+
:root {
|
|
990
|
+
--bg-primary: white;
|
|
991
|
+
--bg-secondary: white;
|
|
992
|
+
--bg-tertiary: #f5f5f5;
|
|
993
|
+
--text-primary: black;
|
|
994
|
+
--text-secondary: #555;
|
|
995
|
+
--text-muted: #888;
|
|
996
|
+
--border: #ddd;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
</style>
|
|
1000
|
+
</head>
|
|
1001
|
+
<body>
|
|
1002
|
+
<div class="container">
|
|
1003
|
+
<header class="header">
|
|
1004
|
+
<div class="header-left">
|
|
1005
|
+
<h1><span class="header-icon">${icons.shield}</span>${escapeHtml(result.benchmark)}</h1>
|
|
1006
|
+
<div class="meta">Version ${result.version} • Generated ${new Date(result.timestamp).toLocaleString()}</div>
|
|
1007
|
+
</div>
|
|
1008
|
+
<div class="header-right">
|
|
1009
|
+
<div class="rating-badge">${result.rating}</div>
|
|
1010
|
+
<div class="level-tag">${result.level} — ${result.level === 'L1' ? 'Essential' : result.level === 'L2' ? 'Standard' : 'Hardened'}</div>
|
|
1011
|
+
</div>
|
|
1012
|
+
</header>
|
|
1013
|
+
|
|
1014
|
+
<div class="dashboard">
|
|
1015
|
+
<div class="score-card">
|
|
1016
|
+
<div class="score-header">
|
|
1017
|
+
<div class="score-grade" style="background: ${grade.color}20; border-color: ${grade.color};">
|
|
1018
|
+
<span class="grade-letter" style="color: ${grade.color};">${grade.letter}</span>
|
|
1019
|
+
</div>
|
|
1020
|
+
<div class="score-main">
|
|
1021
|
+
<div class="score-pct">${result.compliance}%</div>
|
|
1022
|
+
<div class="score-label">Security Score</div>
|
|
1023
|
+
</div>
|
|
1024
|
+
</div>
|
|
1025
|
+
|
|
1026
|
+
<div class="score-bars">
|
|
1027
|
+
<div class="score-bar-row">
|
|
1028
|
+
<span class="bar-label">Passed</span>
|
|
1029
|
+
<div class="bar-track">
|
|
1030
|
+
<div class="bar-fill bar-pass" style="width: ${allControls.length ? (passedControls.length / allControls.length * 100) : 0}%;"></div>
|
|
1031
|
+
</div>
|
|
1032
|
+
<span class="bar-count">${passedControls.length}</span>
|
|
1033
|
+
</div>
|
|
1034
|
+
<div class="score-bar-row">
|
|
1035
|
+
<span class="bar-label">Failed</span>
|
|
1036
|
+
<div class="bar-track">
|
|
1037
|
+
<div class="bar-fill bar-fail" style="width: ${allControls.length ? (failedControls.length / allControls.length * 100) : 0}%;"></div>
|
|
1038
|
+
</div>
|
|
1039
|
+
<span class="bar-count">${failedControls.length}</span>
|
|
1040
|
+
</div>
|
|
1041
|
+
<div class="score-bar-row">
|
|
1042
|
+
<span class="bar-label">Manual</span>
|
|
1043
|
+
<div class="bar-track">
|
|
1044
|
+
<div class="bar-fill bar-manual" style="width: ${allControls.length ? (unverifiedControls.length / allControls.length * 100) : 0}%;"></div>
|
|
1045
|
+
</div>
|
|
1046
|
+
<span class="bar-count">${unverifiedControls.length}</span>
|
|
1047
|
+
</div>
|
|
1048
|
+
</div>
|
|
1049
|
+
|
|
1050
|
+
<div class="level-breakdown">
|
|
1051
|
+
<div class="level-row">
|
|
1052
|
+
<span class="level-badge level-l1">L1</span>
|
|
1053
|
+
<span class="level-stat">${levelStats.L1.passed}/${levelStats.L1.total}</span>
|
|
1054
|
+
</div>
|
|
1055
|
+
<div class="level-row">
|
|
1056
|
+
<span class="level-badge level-l2">L2</span>
|
|
1057
|
+
<span class="level-stat">${levelStats.L2.passed}/${levelStats.L2.total}</span>
|
|
1058
|
+
</div>
|
|
1059
|
+
<div class="level-row">
|
|
1060
|
+
<span class="level-badge level-l3">L3</span>
|
|
1061
|
+
<span class="level-stat">${levelStats.L3.passed}/${levelStats.L3.total}</span>
|
|
1062
|
+
</div>
|
|
1063
|
+
</div>
|
|
1064
|
+
|
|
1065
|
+
${worstCategory && worstCategory.compliance < 100 ? `
|
|
1066
|
+
<div class="worst-category">
|
|
1067
|
+
<span class="worst-label">Needs Attention</span>
|
|
1068
|
+
<span class="worst-name">${escapeHtml(worstCategory.category)}</span>
|
|
1069
|
+
<span class="worst-pct" style="color: ${worstCategory.compliance < 50 ? '#ef4444' : '#eab308'};">${worstCategory.compliance}%</span>
|
|
1070
|
+
</div>` : ''}
|
|
1071
|
+
</div>
|
|
1072
|
+
|
|
1073
|
+
<div class="radar-section">
|
|
1074
|
+
<h3>Category Coverage</h3>
|
|
1075
|
+
<div class="radar-container">
|
|
1076
|
+
<svg width="240" height="240" viewBox="0 0 240 240">
|
|
1077
|
+
${radarGrid}
|
|
1078
|
+
${radarAxes}
|
|
1079
|
+
<polygon points="${radarPoints.join(' ')}" fill="${complianceColor}20" stroke="${complianceColor}" stroke-width="2"/>
|
|
1080
|
+
${radarLabels.join('')}
|
|
1081
|
+
</svg>
|
|
1082
|
+
</div>
|
|
1083
|
+
</div>
|
|
1084
|
+
|
|
1085
|
+
<div class="exec-section">
|
|
1086
|
+
<h3>Priority Issues</h3>
|
|
1087
|
+
${executiveSummary}
|
|
1088
|
+
${failedControls.length > 0 ? `<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border); font-size: 0.8rem; color: var(--text-muted);">
|
|
1089
|
+
${levelDesc}
|
|
1090
|
+
</div>` : ''}
|
|
1091
|
+
</div>
|
|
1092
|
+
</div>
|
|
1093
|
+
|
|
1094
|
+
<div class="categories-header">
|
|
1095
|
+
<h2>Control Details by Category</h2>
|
|
1096
|
+
<button class="expand-all" onclick="toggleAll()">Expand All</button>
|
|
1097
|
+
</div>
|
|
1098
|
+
|
|
1099
|
+
${categoryRows}
|
|
1100
|
+
|
|
1101
|
+
<footer class="footer">
|
|
1102
|
+
<div>
|
|
1103
|
+
Generated by <a href="https://hackmyagent.com">HackMyAgent</a> •
|
|
1104
|
+
<a href="https://oasb.ai">OASB-1 Specification</a>
|
|
1105
|
+
</div>
|
|
1106
|
+
<div class="footer-actions">
|
|
1107
|
+
<button class="footer-btn" onclick="window.print()">${icons.print} Print / PDF</button>
|
|
1108
|
+
</div>
|
|
1109
|
+
</footer>
|
|
1110
|
+
</div>
|
|
1111
|
+
|
|
1112
|
+
<script>
|
|
1113
|
+
function toggleCategory(index) {
|
|
1114
|
+
const cat = document.getElementById('cat-' + index);
|
|
1115
|
+
cat.classList.toggle('collapsed');
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function toggleAll() {
|
|
1119
|
+
const categories = document.querySelectorAll('.category');
|
|
1120
|
+
const btn = document.querySelector('.expand-all');
|
|
1121
|
+
const allCollapsed = Array.from(categories).every(c => c.classList.contains('collapsed'));
|
|
1122
|
+
|
|
1123
|
+
categories.forEach(cat => {
|
|
1124
|
+
if (allCollapsed) {
|
|
1125
|
+
cat.classList.remove('collapsed');
|
|
1126
|
+
} else {
|
|
1127
|
+
cat.classList.add('collapsed');
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
btn.textContent = allCollapsed ? 'Collapse All' : 'Expand All';
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Start with categories collapsed
|
|
1135
|
+
document.querySelectorAll('.category').forEach(cat => cat.classList.add('collapsed'));
|
|
1136
|
+
</script>
|
|
1137
|
+
</body>
|
|
1138
|
+
</html>`;
|
|
1139
|
+
}
|
|
1140
|
+
function escapeHtml(str) {
|
|
1141
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
1142
|
+
}
|
|
1143
|
+
// SARIF output for non-benchmark secure scans
|
|
1144
|
+
function generateScanSarif(findings, targetDir) {
|
|
1145
|
+
const issues = findings.filter(f => !f.passed && !f.fixed);
|
|
1146
|
+
const rules = issues.map(f => ({
|
|
1147
|
+
id: f.checkId,
|
|
1148
|
+
name: f.name.replace(/\s+/g, ''),
|
|
1149
|
+
shortDescription: { text: f.name },
|
|
1150
|
+
fullDescription: { text: f.description },
|
|
1151
|
+
help: { text: f.fix || `Fix the ${f.name} issue.` },
|
|
1152
|
+
defaultConfiguration: {
|
|
1153
|
+
level: (f.severity === 'critical' || f.severity === 'high' ? 'error' :
|
|
1154
|
+
f.severity === 'medium' ? 'warning' : 'note'),
|
|
1155
|
+
},
|
|
1156
|
+
properties: {
|
|
1157
|
+
'security-severity': f.severity === 'critical' ? '9.0' :
|
|
1158
|
+
f.severity === 'high' ? '7.0' :
|
|
1159
|
+
f.severity === 'medium' ? '5.0' : '3.0',
|
|
1160
|
+
tags: ['security', 'ai-agent', f.category],
|
|
1161
|
+
},
|
|
1162
|
+
}));
|
|
1163
|
+
const results = issues.map(f => ({
|
|
1164
|
+
ruleId: f.checkId,
|
|
1165
|
+
level: (f.severity === 'critical' || f.severity === 'high' ? 'error' :
|
|
1166
|
+
f.severity === 'medium' ? 'warning' : 'note'),
|
|
1167
|
+
message: { text: f.description },
|
|
1168
|
+
locations: f.file ? [{
|
|
1169
|
+
physicalLocation: {
|
|
1170
|
+
artifactLocation: { uri: f.file.replace(targetDir + '/', '') },
|
|
1171
|
+
...(f.line ? { region: { startLine: f.line } } : {}),
|
|
1172
|
+
},
|
|
1173
|
+
}] : undefined,
|
|
1174
|
+
}));
|
|
1175
|
+
return JSON.stringify({
|
|
1176
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
1177
|
+
version: '2.1.0',
|
|
1178
|
+
runs: [{
|
|
1179
|
+
tool: {
|
|
1180
|
+
driver: {
|
|
1181
|
+
name: 'HackMyAgent',
|
|
1182
|
+
version: index_1.VERSION,
|
|
1183
|
+
informationUri: 'https://hackmyagent.com',
|
|
1184
|
+
rules,
|
|
1185
|
+
},
|
|
1186
|
+
},
|
|
1187
|
+
results,
|
|
1188
|
+
}],
|
|
1189
|
+
}, null, 2);
|
|
1190
|
+
}
|
|
1191
|
+
// HTML report for non-benchmark secure scans
|
|
1192
|
+
function generateScanHtmlReport(scanResult, targetDir) {
|
|
1193
|
+
const issues = scanResult.findings.filter(f => !f.passed && !f.fixed);
|
|
1194
|
+
const fixedFindings = scanResult.findings.filter(f => f.fixed);
|
|
1195
|
+
const score = scanResult.score;
|
|
1196
|
+
const scoreColor = score >= 90 ? '#22c55e' : score >= 70 ? '#eab308' : score >= 50 ? '#f97316' : '#ef4444';
|
|
1197
|
+
const gradeLetters = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F';
|
|
1198
|
+
const severityOrder = ['critical', 'high', 'medium', 'low'];
|
|
1199
|
+
const severityColors = {
|
|
1200
|
+
critical: '#ef4444', high: '#f97316', medium: '#eab308', low: '#22c55e',
|
|
1201
|
+
};
|
|
1202
|
+
const issueRows = issues
|
|
1203
|
+
.sort((a, b) => severityOrder.indexOf(a.severity) - severityOrder.indexOf(b.severity))
|
|
1204
|
+
.map(f => `
|
|
1205
|
+
<tr>
|
|
1206
|
+
<td><span class="severity-badge" style="background: ${severityColors[f.severity]}20; color: ${severityColors[f.severity]}; border: 1px solid ${severityColors[f.severity]}40;">${escapeHtml(f.severity.toUpperCase())}</span></td>
|
|
1207
|
+
<td><code>${escapeHtml(f.checkId)}</code></td>
|
|
1208
|
+
<td>${escapeHtml(f.description)}</td>
|
|
1209
|
+
<td>${f.file ? escapeHtml(f.file) + (f.line ? ':' + f.line : '') : ''}</td>
|
|
1210
|
+
<td>${f.fix ? escapeHtml(f.fix) : ''}</td>
|
|
1211
|
+
</tr>`).join('');
|
|
1212
|
+
const fixedRows = fixedFindings.map(f => `
|
|
1213
|
+
<tr>
|
|
1214
|
+
<td><span class="severity-badge" style="background: #22c55e20; color: #22c55e; border: 1px solid #22c55e40;">FIXED</span></td>
|
|
1215
|
+
<td><code>${escapeHtml(f.checkId)}</code></td>
|
|
1216
|
+
<td>${escapeHtml(f.description)}</td>
|
|
1217
|
+
<td>${f.file ? escapeHtml(f.file) : ''}</td>
|
|
1218
|
+
<td>${f.fixMessage ? escapeHtml(f.fixMessage) : ''}</td>
|
|
1219
|
+
</tr>`).join('');
|
|
1220
|
+
const projectTypeLabel = {
|
|
1221
|
+
cli: 'CLI Tool', library: 'Library', webapp: 'Web App', api: 'API Server',
|
|
1222
|
+
mcp: 'MCP Server', openclaw: 'OpenClaw Agent', all: 'Project',
|
|
1223
|
+
};
|
|
1224
|
+
return `<!DOCTYPE html>
|
|
1225
|
+
<html lang="en">
|
|
1226
|
+
<head>
|
|
1227
|
+
<meta charset="UTF-8">
|
|
1228
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1229
|
+
<title>HackMyAgent Security Report | ${escapeHtml(require('path').basename(targetDir))}</title>
|
|
1230
|
+
<style>
|
|
1231
|
+
:root { --bg-primary: #0a0f1a; --bg-secondary: #111827; --bg-tertiary: #1f2937; --text-primary: #f1f5f9; --text-secondary: #94a3b8; --border: #334155; }
|
|
1232
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1233
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg-primary); color: var(--text-primary); line-height: 1.6; padding: 2rem; font-size: 14px; }
|
|
1234
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
1235
|
+
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
1236
|
+
.meta { color: var(--text-secondary); margin-bottom: 2rem; }
|
|
1237
|
+
.score-card { display: flex; align-items: center; gap: 2rem; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 12px; padding: 1.5rem; margin-bottom: 2rem; }
|
|
1238
|
+
.grade { font-size: 3rem; font-weight: 700; width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; border-radius: 50%; border: 3px solid ${scoreColor}; }
|
|
1239
|
+
.score-details { flex: 1; }
|
|
1240
|
+
.score-num { font-size: 2rem; font-weight: 700; }
|
|
1241
|
+
.stats { display: flex; gap: 2rem; margin-top: 0.5rem; }
|
|
1242
|
+
.stat { color: var(--text-secondary); }
|
|
1243
|
+
.stat strong { color: var(--text-primary); }
|
|
1244
|
+
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
|
|
1245
|
+
th { text-align: left; padding: 0.75rem; background: var(--bg-secondary); border-bottom: 1px solid var(--border); color: var(--text-secondary); font-size: 0.8rem; text-transform: uppercase; }
|
|
1246
|
+
td { padding: 0.75rem; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
1247
|
+
code { background: var(--bg-tertiary); padding: 2px 6px; border-radius: 4px; font-size: 0.85em; }
|
|
1248
|
+
.severity-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }
|
|
1249
|
+
.section { margin-top: 2rem; }
|
|
1250
|
+
.section h2 { font-size: 1.2rem; margin-bottom: 0.5rem; }
|
|
1251
|
+
footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--border); color: var(--text-secondary); font-size: 0.85rem; }
|
|
1252
|
+
footer a { color: #3b82f6; }
|
|
1253
|
+
@media print { body { background: #fff; color: #000; } .score-card { border-color: #ccc; } th { background: #f3f4f6; } td { border-color: #e5e7eb; } }
|
|
1254
|
+
</style>
|
|
1255
|
+
</head>
|
|
1256
|
+
<body>
|
|
1257
|
+
<div class="container">
|
|
1258
|
+
<h1>HackMyAgent Security Report</h1>
|
|
1259
|
+
<div class="meta">${escapeHtml(projectTypeLabel[scanResult.projectType] || 'Project')} - ${escapeHtml(require('path').basename(targetDir))} - ${new Date().toISOString().split('T')[0]}</div>
|
|
1260
|
+
|
|
1261
|
+
<div class="score-card">
|
|
1262
|
+
<div class="grade" style="color: ${scoreColor};">${gradeLetters}</div>
|
|
1263
|
+
<div class="score-details">
|
|
1264
|
+
<div class="score-num" style="color: ${scoreColor};">${score}/${scanResult.maxScore}</div>
|
|
1265
|
+
<div class="stats">
|
|
1266
|
+
<span class="stat"><strong>${issues.length}</strong> issues</span>
|
|
1267
|
+
<span class="stat"><strong>${fixedFindings.length}</strong> fixed</span>
|
|
1268
|
+
<span class="stat"><strong>${scanResult.findings.filter(f => f.passed).length}</strong> passed</span>
|
|
1269
|
+
</div>
|
|
1270
|
+
</div>
|
|
1271
|
+
</div>
|
|
1272
|
+
|
|
1273
|
+
${issues.length > 0 ? `
|
|
1274
|
+
<div class="section">
|
|
1275
|
+
<h2>Issues (${issues.length})</h2>
|
|
1276
|
+
<table>
|
|
1277
|
+
<thead><tr><th>Severity</th><th>Check</th><th>Description</th><th>Location</th><th>Remediation</th></tr></thead>
|
|
1278
|
+
<tbody>${issueRows}</tbody>
|
|
1279
|
+
</table>
|
|
1280
|
+
</div>` : '<div class="section"><h2>No issues found</h2></div>'}
|
|
1281
|
+
|
|
1282
|
+
${fixedFindings.length > 0 ? `
|
|
1283
|
+
<div class="section">
|
|
1284
|
+
<h2>Auto-Fixed (${fixedFindings.length})</h2>
|
|
1285
|
+
<table>
|
|
1286
|
+
<thead><tr><th>Status</th><th>Check</th><th>Description</th><th>Location</th><th>Details</th></tr></thead>
|
|
1287
|
+
<tbody>${fixedRows}</tbody>
|
|
1288
|
+
</table>
|
|
1289
|
+
</div>` : ''}
|
|
1290
|
+
|
|
1291
|
+
<footer>Generated by <a href="https://hackmyagent.com">HackMyAgent</a> v${index_1.VERSION}</footer>
|
|
1292
|
+
</div>
|
|
1293
|
+
</body>
|
|
1294
|
+
</html>`;
|
|
1295
|
+
}
|
|
1296
|
+
// Agent Security Profile (ASP) - our differentiator format
|
|
1297
|
+
function generateAspOutput(benchmarkResult, scanResult, targetDir) {
|
|
1298
|
+
const fs = require('fs');
|
|
1299
|
+
const path = require('path');
|
|
1300
|
+
// Try to get agent name from package.json or directory name
|
|
1301
|
+
let agentName = path.basename(targetDir);
|
|
1302
|
+
let agentVersion = '0.0.0';
|
|
1303
|
+
try {
|
|
1304
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
1305
|
+
if (fs.existsSync(pkgPath)) {
|
|
1306
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
1307
|
+
agentName = pkg.name || agentName;
|
|
1308
|
+
agentVersion = pkg.version || agentVersion;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
catch { /* ignore */ }
|
|
1312
|
+
// Analyze capabilities from findings
|
|
1313
|
+
const capabilities = {};
|
|
1314
|
+
const hasFilesystemAccess = scanResult.findings.some(f => f.checkId.includes('FS-') || f.description.toLowerCase().includes('filesystem'));
|
|
1315
|
+
const hasNetworkAccess = scanResult.findings.some(f => f.checkId.includes('NET-') || f.description.toLowerCase().includes('network'));
|
|
1316
|
+
const hasShellAccess = scanResult.findings.some(f => f.checkId.includes('SHELL-') || f.description.toLowerCase().includes('shell') || f.description.toLowerCase().includes('exec'));
|
|
1317
|
+
capabilities['filesystem'] = hasFilesystemAccess ? 'detected' : 'none';
|
|
1318
|
+
capabilities['network'] = hasNetworkAccess ? 'detected' : 'none';
|
|
1319
|
+
capabilities['shell'] = hasShellAccess ? 'detected' : 'none';
|
|
1320
|
+
// Credential hygiene
|
|
1321
|
+
const credentialFindings = scanResult.findings.filter(f => f.checkId.startsWith('CRED-'));
|
|
1322
|
+
const hardcodedCreds = credentialFindings.filter(f => !f.passed).length;
|
|
1323
|
+
// Supply chain status
|
|
1324
|
+
const supplyChainFindings = scanResult.findings.filter(f => f.checkId.startsWith('SKILL-') || f.checkId.startsWith('HEARTBEAT-') || f.checkId.startsWith('DEP-'));
|
|
1325
|
+
const signedSkills = !supplyChainFindings.some(f => f.checkId === 'SKILL-001' && !f.passed);
|
|
1326
|
+
const pinnedDeps = !supplyChainFindings.some(f => f.checkId === 'DEP-001' && !f.passed);
|
|
1327
|
+
const asp = {
|
|
1328
|
+
specVersion: '1.0.0',
|
|
1329
|
+
generatedAt: new Date().toISOString(),
|
|
1330
|
+
generator: {
|
|
1331
|
+
name: 'HackMyAgent',
|
|
1332
|
+
version: index_1.VERSION,
|
|
1333
|
+
url: 'https://hackmyagent.com',
|
|
1334
|
+
},
|
|
1335
|
+
agent: {
|
|
1336
|
+
name: agentName,
|
|
1337
|
+
version: agentVersion,
|
|
1338
|
+
type: scanResult.projectType,
|
|
1339
|
+
path: targetDir,
|
|
1340
|
+
},
|
|
1341
|
+
securityPosture: {
|
|
1342
|
+
benchmark: 'OASB-1',
|
|
1343
|
+
benchmarkVersion: benchmarkResult.version,
|
|
1344
|
+
level: benchmarkResult.level,
|
|
1345
|
+
compliance: benchmarkResult.compliance,
|
|
1346
|
+
rating: benchmarkResult.rating,
|
|
1347
|
+
l1Compliance: benchmarkResult.l1Compliance,
|
|
1348
|
+
l2Compliance: benchmarkResult.l2Compliance,
|
|
1349
|
+
l3Compliance: benchmarkResult.l3Compliance,
|
|
1350
|
+
},
|
|
1351
|
+
capabilities,
|
|
1352
|
+
credentials: {
|
|
1353
|
+
hardcodedSecrets: hardcodedCreds,
|
|
1354
|
+
recommendation: hardcodedCreds > 0 ? 'Move secrets to environment variables or secrets manager' : 'No hardcoded credentials detected',
|
|
1355
|
+
},
|
|
1356
|
+
supplyChain: {
|
|
1357
|
+
signedComponents: signedSkills,
|
|
1358
|
+
pinnedDependencies: pinnedDeps,
|
|
1359
|
+
issues: supplyChainFindings.filter(f => !f.passed).map(f => ({
|
|
1360
|
+
id: f.checkId,
|
|
1361
|
+
description: f.description,
|
|
1362
|
+
remediation: f.fix,
|
|
1363
|
+
})),
|
|
1364
|
+
},
|
|
1365
|
+
categories: benchmarkResult.categories.map(cat => ({
|
|
1366
|
+
name: cat.category,
|
|
1367
|
+
compliance: cat.compliance,
|
|
1368
|
+
passed: cat.passed,
|
|
1369
|
+
failed: cat.failed,
|
|
1370
|
+
unverified: cat.unverified,
|
|
1371
|
+
})),
|
|
1372
|
+
failedControls: benchmarkResult.categories.flatMap(cat => cat.controls.filter(c => c.status === 'failed').map(c => ({
|
|
1373
|
+
id: c.controlId,
|
|
1374
|
+
name: c.name,
|
|
1375
|
+
level: c.level,
|
|
1376
|
+
findings: c.findings,
|
|
1377
|
+
remediation: c.remediation,
|
|
1378
|
+
}))),
|
|
1379
|
+
// Attestation placeholder - could be signed in future
|
|
1380
|
+
attestation: {
|
|
1381
|
+
timestamp: new Date().toISOString(),
|
|
1382
|
+
// signature: null, // Future: GPG or Sigstore signature
|
|
1383
|
+
},
|
|
1384
|
+
};
|
|
1385
|
+
return JSON.stringify(asp, null, 2);
|
|
1386
|
+
}
|
|
1387
|
+
function printBenchmarkReport(result, verbose) {
|
|
1388
|
+
const ratingColors = {
|
|
1389
|
+
'Certified': colors.green,
|
|
1390
|
+
'Compliant': colors.green,
|
|
1391
|
+
'Passing': colors.yellow,
|
|
1392
|
+
'Needs Improvement': colors.yellow,
|
|
1393
|
+
'Failing': colors.red,
|
|
1394
|
+
};
|
|
1395
|
+
// Header
|
|
1396
|
+
console.log(`\n📋 ${result.benchmark} v${result.version}`);
|
|
1397
|
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
|
1398
|
+
// Level and rating
|
|
1399
|
+
const levelNames = {
|
|
1400
|
+
'L1': 'Level 1 - Essential',
|
|
1401
|
+
'L2': 'Level 2 - Standard',
|
|
1402
|
+
'L3': 'Level 3 - Hardened',
|
|
1403
|
+
};
|
|
1404
|
+
console.log(`Level: ${levelNames[result.level]}`);
|
|
1405
|
+
console.log(`Rating: ${ratingColors[result.rating]}${result.rating}${RESET()}`);
|
|
1406
|
+
console.log(`Compliance: ${result.compliance}% (${result.passedControls}/${result.passedControls + result.failedControls} verified controls)`);
|
|
1407
|
+
if (result.unverifiedControls > 0) {
|
|
1408
|
+
console.log(`Unverified: ${result.unverifiedControls} controls require manual/forward verification`);
|
|
1409
|
+
}
|
|
1410
|
+
console.log();
|
|
1411
|
+
// Category breakdown
|
|
1412
|
+
console.log(`Categories:`);
|
|
1413
|
+
for (const catResult of result.categories) {
|
|
1414
|
+
const statusIcon = catResult.failed === 0 ? '✅' : (catResult.passed > 0 ? '🟡' : '❌');
|
|
1415
|
+
console.log(` ${statusIcon} ${catResult.category}: ${catResult.passed}/${catResult.passed + catResult.failed} (${catResult.compliance}%)`);
|
|
1416
|
+
// Show failed controls
|
|
1417
|
+
if (verbose || catResult.failed > 0) {
|
|
1418
|
+
for (const ctrl of catResult.controls) {
|
|
1419
|
+
if (ctrl.status === 'failed') {
|
|
1420
|
+
console.log(` ❌ ${ctrl.controlId}: ${ctrl.name}`);
|
|
1421
|
+
if (verbose) {
|
|
1422
|
+
for (const finding of ctrl.findings) {
|
|
1423
|
+
console.log(` └─ ${finding}`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
else if (verbose && ctrl.status === 'passed') {
|
|
1428
|
+
console.log(` ✅ ${ctrl.controlId}: ${ctrl.name}`);
|
|
1429
|
+
}
|
|
1430
|
+
else if (verbose && ctrl.status === 'unverified') {
|
|
1431
|
+
// Look up the original control to determine why it's unverified
|
|
1432
|
+
const originalControl = index_1.OASB_1_CATEGORIES
|
|
1433
|
+
.flatMap((c) => c.controls)
|
|
1434
|
+
.find((c) => c.id === ctrl.controlId);
|
|
1435
|
+
const reason = originalControl && (originalControl.verification === 'manual' || originalControl.verification === 'forward')
|
|
1436
|
+
? 'manual/forward'
|
|
1437
|
+
: 'no scanner data';
|
|
1438
|
+
console.log(` ⚪ ${ctrl.controlId}: ${ctrl.name} (${reason})`);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
console.log();
|
|
1444
|
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
1445
|
+
// Compliance breakdown by level
|
|
1446
|
+
if (verbose) {
|
|
1447
|
+
console.log(`\nCompliance by level: L1=${result.l1Compliance}% L2=${result.l2Compliance}% L3=${result.l3Compliance}%`);
|
|
1448
|
+
console.log(`Legend: ⚪ = Manual/Forward verification required`);
|
|
1449
|
+
}
|
|
1450
|
+
// Show appropriate next step based on current level
|
|
1451
|
+
if (result.level === 'L1') {
|
|
1452
|
+
console.log(`\nRun 'hackmyagent secure -b oasb-1 -l L2' for stricter checks.`);
|
|
1453
|
+
}
|
|
1454
|
+
else if (result.level === 'L2') {
|
|
1455
|
+
console.log(`\nRun 'hackmyagent secure -b oasb-1 -l L3' for hardened requirements.`);
|
|
1456
|
+
}
|
|
1457
|
+
else {
|
|
1458
|
+
console.log(`\nThis is the highest maturity level (L3 - Hardened).`);
|
|
1459
|
+
}
|
|
1460
|
+
console.log(`Spec: https://oasb.ai/oasb-1\n`);
|
|
1461
|
+
}
|
|
1462
|
+
// Package name resolution for community registry reporting
|
|
1463
|
+
function resolvePackageName(targetDir) {
|
|
1464
|
+
try {
|
|
1465
|
+
const fs = require('fs');
|
|
1466
|
+
const path = require('path');
|
|
1467
|
+
const pkgJsonPath = path.join(targetDir, 'package.json');
|
|
1468
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
1469
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
1470
|
+
if (pkg.name)
|
|
1471
|
+
return pkg.name;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
catch { /* ignore */ }
|
|
1475
|
+
// Fallback: use directory name
|
|
1476
|
+
const path = require('path');
|
|
1477
|
+
return path.basename(targetDir);
|
|
1478
|
+
}
|
|
1479
|
+
function resolvePackageVersion(targetDir) {
|
|
1480
|
+
try {
|
|
1481
|
+
const fs = require('fs');
|
|
1482
|
+
const path = require('path');
|
|
1483
|
+
const pkgJsonPath = path.join(targetDir, 'package.json');
|
|
1484
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
1485
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
1486
|
+
if (pkg.version)
|
|
1487
|
+
return pkg.version;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
catch { /* ignore */ }
|
|
1491
|
+
return null;
|
|
1492
|
+
}
|
|
1493
|
+
program
|
|
1494
|
+
.command('secure')
|
|
1495
|
+
.description(`Scan and harden your agent setup
|
|
1496
|
+
|
|
1497
|
+
Performs 147 security checks across 30 categories:
|
|
1498
|
+
• Credentials: API key exposure, secrets in configs
|
|
1499
|
+
• MCP: Server configs, tool permissions, secrets
|
|
1500
|
+
• Network: TLS, interface bindings, CORS
|
|
1501
|
+
• Prompt: Injection defenses, role protection
|
|
1502
|
+
• Encryption: At-rest encryption, secure hashing
|
|
1503
|
+
• And 25 more categories...
|
|
1504
|
+
|
|
1505
|
+
Benchmark mode (--benchmark oasb-1):
|
|
1506
|
+
Run OASB-1 compliance checks with L1/L2/L3 levels.
|
|
1507
|
+
L1 = Essential (baseline), L2 = Standard, L3 = Hardened
|
|
1508
|
+
|
|
1509
|
+
Output formats (--format):
|
|
1510
|
+
text Human-readable terminal output (default)
|
|
1511
|
+
json Machine-readable JSON
|
|
1512
|
+
sarif GitHub Security tab / IDE integration
|
|
1513
|
+
html Shareable compliance report
|
|
1514
|
+
asp Agent Security Profile (our format)
|
|
1515
|
+
|
|
1516
|
+
Severities: critical, high, medium, low
|
|
1517
|
+
Exit code 1 if critical/high issues found (or non-compliant in benchmark mode).
|
|
1518
|
+
|
|
1519
|
+
Examples:
|
|
1520
|
+
$ hackmyagent secure Scan current directory
|
|
1521
|
+
$ hackmyagent secure ./my-project Scan specific directory
|
|
1522
|
+
$ hackmyagent secure --fix Auto-fix issues
|
|
1523
|
+
$ hackmyagent secure -b oasb-1 OASB-1 L1 compliance
|
|
1524
|
+
$ hackmyagent secure -b oasb-1 -l L2 OASB-1 L2 compliance
|
|
1525
|
+
$ hackmyagent secure -b oasb-1 -f sarif SARIF for GitHub
|
|
1526
|
+
$ hackmyagent secure -b oasb-1 -f html -o report.html
|
|
1527
|
+
$ hackmyagent secure -b oasb-1 --fail-below 80 CI threshold`)
|
|
1528
|
+
.argument('[directory]', 'Directory to scan (defaults to current directory)', '.')
|
|
1529
|
+
.option('--fix', 'Automatically fix issues where possible')
|
|
1530
|
+
.option('--dry-run', 'Preview fixes without applying them (use with --fix)')
|
|
1531
|
+
.option('--ignore <checks>', 'Comma-separated check IDs to skip (e.g., CRED-001,GIT-002)')
|
|
1532
|
+
.option('--json', 'Output as JSON (deprecated: use --format json)')
|
|
1533
|
+
.option('-f, --format <format>', 'Output format: text, json, sarif, html, asp (default: text)', 'text')
|
|
1534
|
+
.option('-o, --output <file>', 'Write output to file instead of stdout')
|
|
1535
|
+
.option('--fail-below <percent>', 'Exit 1 if compliance below threshold (0-100)')
|
|
1536
|
+
.option('-v, --verbose', 'Show all checks including passed ones')
|
|
1537
|
+
.option('-b, --benchmark <name>', 'Run benchmark compliance check (e.g., oasb-1)')
|
|
1538
|
+
.option('-l, --level <level>', 'Benchmark level: L1 (Essential), L2 (Standard), L3 (Hardened)', 'L1')
|
|
1539
|
+
.option('-c, --category <name>', 'Filter to specific benchmark category')
|
|
1540
|
+
.option('--deep', 'Enable LLM-powered semantic analysis (requires ANTHROPIC_API_KEY)')
|
|
1541
|
+
.option('--registry-report', 'Post results to OpenA2A Registry')
|
|
1542
|
+
.option('--no-registry', 'Skip auto-publishing results to OpenA2A Registry')
|
|
1543
|
+
.option('--version-id <id>', 'Registry version ID to report against')
|
|
1544
|
+
.option('--registry-url <url>', 'Registry URL (default: REGISTRY_URL env)')
|
|
1545
|
+
.option('--registry-key <key>', 'Registry API key (default: REGISTRY_API_KEY env)')
|
|
1546
|
+
.action(async (directory, options) => {
|
|
1547
|
+
try {
|
|
1548
|
+
const targetDir = directory.startsWith('/') ? directory : process.cwd() + '/' + directory;
|
|
1549
|
+
// Check if directory exists
|
|
1550
|
+
if (!require('fs').existsSync(targetDir)) {
|
|
1551
|
+
console.error(`Error: Directory '${targetDir}' does not exist.`);
|
|
1552
|
+
process.exit(1);
|
|
1553
|
+
}
|
|
1554
|
+
// Parse ignore list
|
|
1555
|
+
const ignoreList = options.ignore
|
|
1556
|
+
? options.ignore.split(',').map((s) => s.trim()).filter(Boolean)
|
|
1557
|
+
: [];
|
|
1558
|
+
// Validate benchmark flag if provided
|
|
1559
|
+
if (options.benchmark && !(0, index_1.isValidBenchmark)(options.benchmark)) {
|
|
1560
|
+
console.error(`Error: Unknown benchmark '${options.benchmark}'. Available: ${index_1.AVAILABLE_BENCHMARKS.join(', ')}`);
|
|
1561
|
+
process.exit(1);
|
|
1562
|
+
}
|
|
1563
|
+
// Validate level if benchmark mode
|
|
1564
|
+
const validLevels = ['L1', 'L2', 'L3'];
|
|
1565
|
+
const level = (options.level?.toUpperCase() || 'L1');
|
|
1566
|
+
if (options.benchmark && !validLevels.includes(level)) {
|
|
1567
|
+
console.error(`Error: Invalid level '${options.level}'. Use: L1, L2, or L3`);
|
|
1568
|
+
process.exit(1);
|
|
1569
|
+
}
|
|
1570
|
+
// Determine output format (--json is deprecated alias for --format json)
|
|
1571
|
+
const validFormats = ['text', 'json', 'sarif', 'html', 'asp'];
|
|
1572
|
+
const format = options.json ? 'json' : (options.format || 'text');
|
|
1573
|
+
if (!validFormats.includes(format)) {
|
|
1574
|
+
console.error(`Error: Invalid format '${format}'. Use: ${validFormats.join(', ')}`);
|
|
1575
|
+
process.exit(1);
|
|
1576
|
+
}
|
|
1577
|
+
// Parse fail threshold
|
|
1578
|
+
const failBelow = options.failBelow ? parseInt(options.failBelow, 10) : undefined;
|
|
1579
|
+
if (failBelow !== undefined && (isNaN(failBelow) || failBelow < 0 || failBelow > 100)) {
|
|
1580
|
+
console.error(`Error: --fail-below must be a number between 0 and 100`);
|
|
1581
|
+
process.exit(1);
|
|
1582
|
+
}
|
|
1583
|
+
// Only show progress for text output
|
|
1584
|
+
if (format === 'text') {
|
|
1585
|
+
if (options.dryRun) {
|
|
1586
|
+
console.log(`\n🔍 Scanning ${targetDir} (dry-run)...\n`);
|
|
1587
|
+
}
|
|
1588
|
+
else {
|
|
1589
|
+
console.log(`\n🔍 Scanning ${targetDir}...\n`);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
// Deep mode progress display
|
|
1593
|
+
const isDeep = options.deep ?? false;
|
|
1594
|
+
const onProgress = isDeep && format === 'text'
|
|
1595
|
+
? (msg) => process.stdout.write(msg)
|
|
1596
|
+
: undefined;
|
|
1597
|
+
if (isDeep && format === 'text') {
|
|
1598
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
1599
|
+
console.log(`Layer 3: Semantic analysis — skipped (no ANTHROPIC_API_KEY)`);
|
|
1600
|
+
console.log(` Tip: Add HackMyAgent as an MCP server for free LLM analysis:`);
|
|
1601
|
+
console.log(` npx hackmyagent init-mcp\n`);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
const scanner = new index_1.HardeningScanner();
|
|
1605
|
+
const result = await scanner.scan({
|
|
1606
|
+
targetDir,
|
|
1607
|
+
autoFix: options.fix ?? false,
|
|
1608
|
+
dryRun: options.dryRun ?? false,
|
|
1609
|
+
ignore: ignoreList,
|
|
1610
|
+
deep: isDeep,
|
|
1611
|
+
onProgress,
|
|
1612
|
+
});
|
|
1613
|
+
// Benchmark mode - output compliance report
|
|
1614
|
+
if (options.benchmark) {
|
|
1615
|
+
// Use allFindings (unfiltered) for accurate benchmark evaluation
|
|
1616
|
+
const benchmarkResult = generateBenchmarkReport(result.allFindings || result.findings, level, options.category);
|
|
1617
|
+
// Output based on format
|
|
1618
|
+
let output;
|
|
1619
|
+
switch (format) {
|
|
1620
|
+
case 'json':
|
|
1621
|
+
output = JSON.stringify(benchmarkResult, null, 2);
|
|
1622
|
+
break;
|
|
1623
|
+
case 'sarif':
|
|
1624
|
+
output = generateSarifOutput(benchmarkResult, result.findings, targetDir);
|
|
1625
|
+
break;
|
|
1626
|
+
case 'html':
|
|
1627
|
+
output = generateHtmlReport(benchmarkResult);
|
|
1628
|
+
break;
|
|
1629
|
+
case 'asp':
|
|
1630
|
+
output = generateAspOutput(benchmarkResult, result, targetDir);
|
|
1631
|
+
break;
|
|
1632
|
+
default: // text
|
|
1633
|
+
printBenchmarkReport(benchmarkResult, options.verbose ?? false);
|
|
1634
|
+
output = '';
|
|
1635
|
+
}
|
|
1636
|
+
// Write output
|
|
1637
|
+
if (output) {
|
|
1638
|
+
if (options.output) {
|
|
1639
|
+
require('fs').writeFileSync(options.output, output);
|
|
1640
|
+
console.error(`Report written to ${options.output}`);
|
|
1641
|
+
}
|
|
1642
|
+
else {
|
|
1643
|
+
console.log(output);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
// Check fail threshold
|
|
1647
|
+
if (failBelow !== undefined && benchmarkResult.compliance < failBelow) {
|
|
1648
|
+
console.error(`Compliance ${benchmarkResult.compliance}% is below threshold ${failBelow}%`);
|
|
1649
|
+
process.exit(1);
|
|
1650
|
+
}
|
|
1651
|
+
// Exit with non-zero if failing or needs improvement (default behavior)
|
|
1652
|
+
if (failBelow === undefined && (benchmarkResult.rating === 'Failing' || benchmarkResult.rating === 'Needs Improvement')) {
|
|
1653
|
+
process.exit(1);
|
|
1654
|
+
}
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
if (format === 'json') {
|
|
1658
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
// Handle SARIF/HTML/ASP for non-benchmark mode
|
|
1662
|
+
if (format === 'sarif') {
|
|
1663
|
+
const output = generateScanSarif(result.findings, targetDir);
|
|
1664
|
+
if (options.output) {
|
|
1665
|
+
require('fs').writeFileSync(options.output, output);
|
|
1666
|
+
console.error(`Report written to ${options.output}`);
|
|
1667
|
+
}
|
|
1668
|
+
else {
|
|
1669
|
+
console.log(output);
|
|
1670
|
+
}
|
|
1671
|
+
const critHigh = result.findings.filter((f) => !f.passed && !f.fixed && (f.severity === 'critical' || f.severity === 'high'));
|
|
1672
|
+
if (critHigh.length > 0)
|
|
1673
|
+
process.exit(1);
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
if (format === 'html') {
|
|
1677
|
+
const output = generateScanHtmlReport(result, targetDir);
|
|
1678
|
+
if (options.output) {
|
|
1679
|
+
require('fs').writeFileSync(options.output, output);
|
|
1680
|
+
console.error(`Report written to ${options.output}`);
|
|
1681
|
+
}
|
|
1682
|
+
else {
|
|
1683
|
+
console.log(output);
|
|
1684
|
+
}
|
|
1685
|
+
const critHigh = result.findings.filter((f) => !f.passed && !f.fixed && (f.severity === 'critical' || f.severity === 'high'));
|
|
1686
|
+
if (critHigh.length > 0)
|
|
1687
|
+
process.exit(1);
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
// Filter to only show failed findings (issues)
|
|
1691
|
+
const issues = result.findings.filter((f) => !f.passed && !f.fixed);
|
|
1692
|
+
const fixedFindings = result.findings.filter((f) => f.fixed);
|
|
1693
|
+
// Print header - clean and simple
|
|
1694
|
+
const projectTypeLabel = {
|
|
1695
|
+
cli: 'CLI Tool',
|
|
1696
|
+
library: 'Library',
|
|
1697
|
+
webapp: 'Web App',
|
|
1698
|
+
api: 'API Server',
|
|
1699
|
+
mcp: 'MCP Server',
|
|
1700
|
+
openclaw: 'OpenClaw Agent',
|
|
1701
|
+
all: 'Project',
|
|
1702
|
+
}[result.projectType] || 'Project';
|
|
1703
|
+
let scoreExtra = '';
|
|
1704
|
+
if (result.semanticAnalysis) {
|
|
1705
|
+
const sa = result.semanticAnalysis;
|
|
1706
|
+
scoreExtra = ` | Semantic: ${sa.layer2Findings} structural`;
|
|
1707
|
+
if (sa.layer3Findings > 0) {
|
|
1708
|
+
scoreExtra += `, ${sa.layer3Findings} LLM`;
|
|
1709
|
+
if (sa.llmCost !== undefined)
|
|
1710
|
+
scoreExtra += ` ($${sa.llmCost.toFixed(3)})`;
|
|
1711
|
+
if (sa.cachedResults)
|
|
1712
|
+
scoreExtra += ` (${sa.cachedResults} cached)`;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
console.log(`${projectTypeLabel} | Score: ${result.score}/${result.maxScore}${scoreExtra}\n`);
|
|
1716
|
+
// No issues? Say so and exit
|
|
1717
|
+
if (issues.length === 0 && fixedFindings.length === 0) {
|
|
1718
|
+
console.log(`${colors.green}No issues found.${RESET()}\n`);
|
|
1719
|
+
}
|
|
1720
|
+
else if (issues.length > 0) {
|
|
1721
|
+
// Print issues - clean format
|
|
1722
|
+
console.log(`${issues.length} issue${issues.length === 1 ? '' : 's'} found:\n`);
|
|
1723
|
+
for (const finding of issues) {
|
|
1724
|
+
const display = SEVERITY_DISPLAY[finding.severity];
|
|
1725
|
+
const location = finding.file
|
|
1726
|
+
? finding.line
|
|
1727
|
+
? `${finding.file}:${finding.line}`
|
|
1728
|
+
: finding.file
|
|
1729
|
+
: '';
|
|
1730
|
+
// Format: SEVERITY file:line
|
|
1731
|
+
// Description
|
|
1732
|
+
// Fix: command
|
|
1733
|
+
console.log(`${display.color()}${display.symbol} ${finding.severity.toUpperCase()}${RESET()} ${location}`);
|
|
1734
|
+
console.log(` ${finding.description}`);
|
|
1735
|
+
if (finding.fix) {
|
|
1736
|
+
console.log(` ${colors.cyan}Fix:${RESET()} ${finding.fix}`);
|
|
1737
|
+
}
|
|
1738
|
+
console.log();
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
// Print fixed findings
|
|
1742
|
+
if (fixedFindings.length > 0) {
|
|
1743
|
+
console.log(`${colors.green}Fixed ${fixedFindings.length} issue${fixedFindings.length === 1 ? '' : 's'}:${RESET()}`);
|
|
1744
|
+
for (const finding of fixedFindings) {
|
|
1745
|
+
const location = finding.file || '';
|
|
1746
|
+
console.log(` ${colors.green}✓${RESET()} ${location} - ${finding.name}`);
|
|
1747
|
+
}
|
|
1748
|
+
console.log();
|
|
1749
|
+
if (result.backupPath) {
|
|
1750
|
+
console.log(`Backup: ${result.backupPath}`);
|
|
1751
|
+
console.log(`Undo: hackmyagent rollback ${directory}\n`);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
// Registry reporting: auto-publish to community endpoint by default
|
|
1755
|
+
const shouldReport = options.registryReport || (options.registry !== false);
|
|
1756
|
+
if (shouldReport) {
|
|
1757
|
+
try {
|
|
1758
|
+
const core = await Promise.resolve().then(() => __importStar(require('./index')));
|
|
1759
|
+
const registryUrl = options.registryUrl || process.env.REGISTRY_URL || 'https://registry.opena2a.org';
|
|
1760
|
+
if (options.versionId) {
|
|
1761
|
+
// Authenticated path: existing behavior (version-id + API key)
|
|
1762
|
+
const registryKey = options.registryKey || process.env.REGISTRY_API_KEY;
|
|
1763
|
+
if (!registryKey) {
|
|
1764
|
+
console.error('Error: --registry-key or REGISTRY_API_KEY env is required when using --version-id');
|
|
1765
|
+
process.exit(1);
|
|
1766
|
+
}
|
|
1767
|
+
const client = new core.RegistryClient({ registryUrl, apiKey: registryKey });
|
|
1768
|
+
const payload = core.buildScanReport(options.versionId, result.findings);
|
|
1769
|
+
await client.reportScanResult(payload);
|
|
1770
|
+
console.log(`Registry: scan results reported for version ${options.versionId}`);
|
|
1771
|
+
}
|
|
1772
|
+
else if (typeof core.buildCommunityReport === 'function') {
|
|
1773
|
+
// Community path: request scan token, then submit results
|
|
1774
|
+
const client = new core.RegistryClient({ registryUrl, apiKey: '' });
|
|
1775
|
+
const packageName = resolvePackageName(targetDir);
|
|
1776
|
+
if (packageName) {
|
|
1777
|
+
const packageVersion = resolvePackageVersion(targetDir);
|
|
1778
|
+
const tokenResp = typeof client.requestScanToken === 'function'
|
|
1779
|
+
? await client.requestScanToken(packageName, { version: packageVersion ?? undefined })
|
|
1780
|
+
: null;
|
|
1781
|
+
const payload = core.buildCommunityReport(packageName, result.findings, {
|
|
1782
|
+
version: packageVersion ?? undefined,
|
|
1783
|
+
});
|
|
1784
|
+
const resp = typeof client.reportCommunityResult === 'function'
|
|
1785
|
+
? await client.reportCommunityResult(payload, tokenResp?.scanToken)
|
|
1786
|
+
: { status: 'skipped' };
|
|
1787
|
+
if (resp.status === 'accepted') {
|
|
1788
|
+
console.log('Registry: scan shared with OpenA2A community');
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
catch (reportErr) {
|
|
1794
|
+
console.error(`Registry: failed to report scan results: ${reportErr.message || reportErr}`);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
// Star prompt (interactive TTY only, text format only)
|
|
1798
|
+
if (process.stdout.isTTY) {
|
|
1799
|
+
console.log(`${colors.cyan}Helpful?${RESET()} Star the project: https://github.com/opena2a-org/opena2a\n`);
|
|
1800
|
+
}
|
|
1801
|
+
// Exit with non-zero if critical/high issues remain
|
|
1802
|
+
const criticalOrHigh = issues.filter((f) => f.severity === 'critical' || f.severity === 'high');
|
|
1803
|
+
if (criticalOrHigh.length > 0) {
|
|
1804
|
+
process.exit(1);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
catch (error) {
|
|
1808
|
+
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
1809
|
+
process.exit(1);
|
|
1810
|
+
}
|
|
1811
|
+
});
|
|
1812
|
+
// Severity display for external scan findings
|
|
1813
|
+
const FINDING_SEVERITY_DISPLAY = {
|
|
1814
|
+
critical: { symbol: '🔴', color: () => colors.brightRed },
|
|
1815
|
+
high: { symbol: '🟠', color: () => colors.red },
|
|
1816
|
+
medium: { symbol: '🟡', color: () => colors.yellow },
|
|
1817
|
+
low: { symbol: '🟢', color: () => colors.green },
|
|
1818
|
+
};
|
|
1819
|
+
function groupExternalFindingsBySeverity(findings) {
|
|
1820
|
+
const grouped = {
|
|
1821
|
+
critical: [],
|
|
1822
|
+
high: [],
|
|
1823
|
+
medium: [],
|
|
1824
|
+
low: [],
|
|
1825
|
+
};
|
|
1826
|
+
for (const finding of findings) {
|
|
1827
|
+
grouped[finding.severity].push(finding);
|
|
1828
|
+
}
|
|
1829
|
+
return grouped;
|
|
1830
|
+
}
|
|
1831
|
+
// OpenClaw-specific check categories
|
|
1832
|
+
const OPENCLAW_CATEGORIES = ['skill', 'heartbeat', 'gateway', 'config', 'supply'];
|
|
1833
|
+
function detectOpenClawDirectory(providedDir) {
|
|
1834
|
+
const os = require('os');
|
|
1835
|
+
const fs = require('fs');
|
|
1836
|
+
const path = require('path');
|
|
1837
|
+
// If user provided a directory, use it
|
|
1838
|
+
if (providedDir && providedDir !== '') {
|
|
1839
|
+
return providedDir.startsWith('/') ? providedDir : path.join(process.cwd(), providedDir);
|
|
1840
|
+
}
|
|
1841
|
+
// Auto-detect common OpenClaw/Moltbot installation directories
|
|
1842
|
+
const homeDir = os.homedir();
|
|
1843
|
+
const candidates = [
|
|
1844
|
+
path.join(homeDir, '.openclaw'),
|
|
1845
|
+
path.join(homeDir, '.moltbot'),
|
|
1846
|
+
path.join(homeDir, '.clawdbot'),
|
|
1847
|
+
];
|
|
1848
|
+
for (const candidate of candidates) {
|
|
1849
|
+
if (fs.existsSync(candidate)) {
|
|
1850
|
+
return candidate;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
// Fall back to current working directory
|
|
1854
|
+
return process.cwd();
|
|
1855
|
+
}
|
|
1856
|
+
function filterOpenClawFindings(findings) {
|
|
1857
|
+
return findings.filter((f) => {
|
|
1858
|
+
const checkId = f.checkId.toLowerCase();
|
|
1859
|
+
return OPENCLAW_CATEGORIES.some((cat) => checkId.includes(cat));
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
function assessRiskLevel(findings) {
|
|
1863
|
+
const criticalCount = findings.filter((f) => f.severity === 'critical').length;
|
|
1864
|
+
const highCount = findings.filter((f) => f.severity === 'high').length;
|
|
1865
|
+
const mediumCount = findings.filter((f) => f.severity === 'medium').length;
|
|
1866
|
+
if (criticalCount > 0) {
|
|
1867
|
+
return {
|
|
1868
|
+
level: 'CRITICAL',
|
|
1869
|
+
color: colors.brightRed,
|
|
1870
|
+
description: 'Immediate action required. Your OpenClaw installation has critical vulnerabilities.',
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
if (highCount > 0) {
|
|
1874
|
+
return {
|
|
1875
|
+
level: 'HIGH',
|
|
1876
|
+
color: colors.red,
|
|
1877
|
+
description: 'Significant risks detected. Address high-severity issues promptly.',
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
if (mediumCount > 0) {
|
|
1881
|
+
return {
|
|
1882
|
+
level: 'MODERATE',
|
|
1883
|
+
color: colors.yellow,
|
|
1884
|
+
description: 'Some issues found. Review and address when possible.',
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
return {
|
|
1888
|
+
level: 'LOW',
|
|
1889
|
+
color: colors.green,
|
|
1890
|
+
description: 'Your OpenClaw installation appears well-secured.',
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
program
|
|
1894
|
+
.command('secure-openclaw')
|
|
1895
|
+
.description(`Security scan specifically for OpenClaw/Moltbot installations
|
|
1896
|
+
|
|
1897
|
+
Performs focused security checks for OpenClaw agent deployments:
|
|
1898
|
+
• Skill validation: Permission scopes, signature verification
|
|
1899
|
+
• Heartbeat security: Endpoint exposure, authentication
|
|
1900
|
+
• Gateway configs: Routing rules, rate limiting
|
|
1901
|
+
• Config files: Secret exposure, insecure defaults
|
|
1902
|
+
• Supply chain: Dependency vulnerabilities, integrity
|
|
1903
|
+
|
|
1904
|
+
Auto-detects ~/.openclaw, ~/.moltbot, or ~/.clawdbot directories.
|
|
1905
|
+
Exit code 1 if critical/high issues found.
|
|
1906
|
+
|
|
1907
|
+
Examples:
|
|
1908
|
+
$ hackmyagent secure-openclaw Scan auto-detected directory
|
|
1909
|
+
$ hackmyagent secure-openclaw ~/.openclaw Scan specific directory
|
|
1910
|
+
$ hackmyagent secure-openclaw --fix Auto-fix issues
|
|
1911
|
+
$ hackmyagent secure-openclaw --json JSON output for CI`)
|
|
1912
|
+
.argument('[directory]', 'Directory to scan (default: ~/.openclaw or ~/.moltbot)', '')
|
|
1913
|
+
.option('--fix', 'Automatically fix issues where possible')
|
|
1914
|
+
.option('--dry-run', 'Preview fixes without applying them (use with --fix)')
|
|
1915
|
+
.option('--json', 'Output as JSON (for scripting/CI)')
|
|
1916
|
+
.option('-v, --verbose', 'Show all checks including passed ones')
|
|
1917
|
+
.action(async (directory, options) => {
|
|
1918
|
+
try {
|
|
1919
|
+
const targetDir = detectOpenClawDirectory(directory);
|
|
1920
|
+
if (!options.json) {
|
|
1921
|
+
console.log(`\n🦞 OpenClaw Security Report`);
|
|
1922
|
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
|
1923
|
+
if (options.dryRun) {
|
|
1924
|
+
console.log(`🔍 Scanning ${targetDir} (dry-run - previewing fixes)...\n`);
|
|
1925
|
+
}
|
|
1926
|
+
else if (options.fix) {
|
|
1927
|
+
console.log(`🔧 Scanning and fixing ${targetDir}...\n`);
|
|
1928
|
+
console.log(`${colors.yellow}Auto-fix will:${RESET()}`);
|
|
1929
|
+
console.log(` • Bind gateway to 127.0.0.1 (local-only)`);
|
|
1930
|
+
console.log(` • Replace plaintext tokens with env var references`);
|
|
1931
|
+
console.log(` • Enable approval confirmations`);
|
|
1932
|
+
console.log(` • Enable sandbox mode`);
|
|
1933
|
+
console.log(`\n${colors.cyan}A backup will be created for rollback if needed.${RESET()}\n`);
|
|
1934
|
+
}
|
|
1935
|
+
else {
|
|
1936
|
+
console.log(`🔍 Scanning ${targetDir}...\n`);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
const scanner = new index_1.HardeningScanner();
|
|
1940
|
+
const result = await scanner.scan({
|
|
1941
|
+
targetDir,
|
|
1942
|
+
autoFix: options.fix ?? false,
|
|
1943
|
+
dryRun: options.dryRun ?? false,
|
|
1944
|
+
ignore: [],
|
|
1945
|
+
});
|
|
1946
|
+
// Filter to OpenClaw-specific findings
|
|
1947
|
+
const allOpenClawFindings = filterOpenClawFindings(result.findings);
|
|
1948
|
+
const issues = allOpenClawFindings.filter((f) => !f.passed && !f.fixed);
|
|
1949
|
+
const fixedFindings = allOpenClawFindings.filter((f) => f.fixed);
|
|
1950
|
+
const passedFindings = allOpenClawFindings.filter((f) => f.passed);
|
|
1951
|
+
if (options.json) {
|
|
1952
|
+
const jsonOutput = {
|
|
1953
|
+
target: targetDir,
|
|
1954
|
+
riskLevel: assessRiskLevel(issues).level,
|
|
1955
|
+
totalChecks: allOpenClawFindings.length,
|
|
1956
|
+
issues: issues.length,
|
|
1957
|
+
fixed: fixedFindings.length,
|
|
1958
|
+
passed: passedFindings.length,
|
|
1959
|
+
findings: allOpenClawFindings,
|
|
1960
|
+
};
|
|
1961
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
// Risk assessment
|
|
1965
|
+
const risk = assessRiskLevel(issues);
|
|
1966
|
+
console.log(`Risk Level: ${risk.color}${risk.level}${RESET()}`);
|
|
1967
|
+
console.log(`${risk.description}\n`);
|
|
1968
|
+
// Summary stats
|
|
1969
|
+
console.log(`Checks: ${allOpenClawFindings.length} total | ${issues.length} issues | ${fixedFindings.length} fixed | ${passedFindings.length} passed\n`);
|
|
1970
|
+
// Show issues
|
|
1971
|
+
if (issues.length > 0) {
|
|
1972
|
+
console.log(`${colors.red}Issues Found:${RESET()}\n`);
|
|
1973
|
+
for (const finding of issues) {
|
|
1974
|
+
const display = SEVERITY_DISPLAY[finding.severity];
|
|
1975
|
+
const location = finding.file
|
|
1976
|
+
? finding.line
|
|
1977
|
+
? `${finding.file}:${finding.line}`
|
|
1978
|
+
: finding.file
|
|
1979
|
+
: '';
|
|
1980
|
+
console.log(`${display.color()}${display.symbol} [${finding.checkId}] ${finding.severity.toUpperCase()}${RESET()}`);
|
|
1981
|
+
console.log(` ${finding.description}`);
|
|
1982
|
+
if (location) {
|
|
1983
|
+
console.log(` File: ${location}`);
|
|
1984
|
+
}
|
|
1985
|
+
if (finding.fix) {
|
|
1986
|
+
console.log(` ${colors.cyan}Fix:${RESET()} ${finding.fix}`);
|
|
1987
|
+
}
|
|
1988
|
+
console.log();
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
else {
|
|
1992
|
+
console.log(`${colors.green}No OpenClaw-specific issues found.${RESET()}\n`);
|
|
1993
|
+
}
|
|
1994
|
+
// Show fixed findings
|
|
1995
|
+
if (fixedFindings.length > 0) {
|
|
1996
|
+
console.log(`${colors.green}✅ Auto-Remediation Applied:${RESET()}\n`);
|
|
1997
|
+
for (const finding of fixedFindings) {
|
|
1998
|
+
console.log(` ${colors.green}✓${RESET()} [${finding.checkId}] ${finding.name}`);
|
|
1999
|
+
if (finding.fixMessage) {
|
|
2000
|
+
console.log(` ${colors.cyan}→${RESET()} ${finding.fixMessage}`);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
console.log();
|
|
2004
|
+
if (result.backupPath) {
|
|
2005
|
+
console.log(`${colors.yellow}📁 Backup created:${RESET()} ${result.backupPath}`);
|
|
2006
|
+
console.log(`${colors.yellow}↩️ To rollback:${RESET()} hackmyagent rollback ${targetDir}`);
|
|
2007
|
+
console.log();
|
|
2008
|
+
console.log(`${colors.cyan}Note:${RESET()} If you replaced tokens with env vars, set OPENCLAW_AUTH_TOKEN`);
|
|
2009
|
+
console.log(` in your environment before starting OpenClaw.\n`);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
// Show passed checks in verbose mode
|
|
2013
|
+
if (options.verbose && passedFindings.length > 0) {
|
|
2014
|
+
console.log(`${colors.green}Passed Checks:${RESET()}`);
|
|
2015
|
+
for (const finding of passedFindings) {
|
|
2016
|
+
console.log(` ${colors.green}✓${RESET()} [${finding.checkId}] ${finding.name}`);
|
|
2017
|
+
}
|
|
2018
|
+
console.log();
|
|
2019
|
+
}
|
|
2020
|
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
2021
|
+
console.log(`Run 'hackmyagent secure' for a full security scan.\n`);
|
|
2022
|
+
// Exit with non-zero if critical/high issues remain
|
|
2023
|
+
const criticalOrHigh = issues.filter((f) => f.severity === 'critical' || f.severity === 'high');
|
|
2024
|
+
if (criticalOrHigh.length > 0) {
|
|
2025
|
+
process.exit(1);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
catch (error) {
|
|
2029
|
+
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
2030
|
+
process.exit(1);
|
|
2031
|
+
}
|
|
2032
|
+
});
|
|
2033
|
+
program
|
|
2034
|
+
.command('scan')
|
|
2035
|
+
.description(`Scan external target for exposed MCP endpoints
|
|
2036
|
+
|
|
2037
|
+
Detects externally exposed:
|
|
2038
|
+
• MCP SSE/tools endpoints
|
|
2039
|
+
• Configuration files (mcp.json, settings)
|
|
2040
|
+
• API keys in responses
|
|
2041
|
+
• Debug/admin interfaces
|
|
2042
|
+
|
|
2043
|
+
Scoring: A (90-100), B (80-89), C (70-79), D (60-69), F (<60)
|
|
2044
|
+
Exit code 1 if critical/high issues found.
|
|
2045
|
+
|
|
2046
|
+
Examples:
|
|
2047
|
+
$ hackmyagent scan example.com
|
|
2048
|
+
$ hackmyagent scan 192.168.1.100 -p 3000,8080
|
|
2049
|
+
$ hackmyagent scan example.com --verbose
|
|
2050
|
+
$ hackmyagent scan example.com --json`)
|
|
2051
|
+
.argument('<target>', 'Target hostname or IP address')
|
|
2052
|
+
.option('--json', 'Output as JSON (for scripting/CI)')
|
|
2053
|
+
.option('-p, --ports <ports>', 'Comma-separated ports to scan (default: common MCP ports)')
|
|
2054
|
+
.option('-t, --timeout <ms>', 'Connection timeout in milliseconds', '5000')
|
|
2055
|
+
.option('-v, --verbose', 'Show detailed finding information')
|
|
2056
|
+
.action(async (target, options) => {
|
|
2057
|
+
try {
|
|
2058
|
+
console.log(`\n🔍 Scanning ${target}...\n`);
|
|
2059
|
+
const scanner = new index_1.ExternalScanner();
|
|
2060
|
+
const customPorts = options.ports
|
|
2061
|
+
? options.ports.split(',').map((p) => parseInt(p.trim(), 10))
|
|
2062
|
+
: undefined;
|
|
2063
|
+
const result = await scanner.scan(target, {
|
|
2064
|
+
ports: customPorts,
|
|
2065
|
+
timeout: parseInt(options.timeout ?? '5000', 10),
|
|
2066
|
+
});
|
|
2067
|
+
if (options.json) {
|
|
2068
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
// Print header
|
|
2072
|
+
const gradeColor = result.grade === 'A'
|
|
2073
|
+
? colors.green
|
|
2074
|
+
: result.grade === 'B'
|
|
2075
|
+
? colors.green
|
|
2076
|
+
: result.grade === 'C'
|
|
2077
|
+
? colors.yellow
|
|
2078
|
+
: colors.red;
|
|
2079
|
+
console.log(`Target: ${result.target}`);
|
|
2080
|
+
console.log(`Score: ${gradeColor}${result.score}/100 (${result.grade})${RESET()}`);
|
|
2081
|
+
console.log(`Open Ports: ${result.openPorts.length > 0 ? result.openPorts.join(', ') : 'None detected'}`);
|
|
2082
|
+
console.log(`Duration: ${result.duration}ms\n`);
|
|
2083
|
+
if (result.findings.length === 0) {
|
|
2084
|
+
console.log(`${colors.green}✅ No security issues found!${RESET()}\n`);
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
// Group findings by severity
|
|
2088
|
+
const grouped = groupExternalFindingsBySeverity(result.findings);
|
|
2089
|
+
// Print findings by severity
|
|
2090
|
+
for (const severity of ['critical', 'high', 'medium', 'low']) {
|
|
2091
|
+
const findings = grouped[severity];
|
|
2092
|
+
if (findings.length === 0)
|
|
2093
|
+
continue;
|
|
2094
|
+
const display = FINDING_SEVERITY_DISPLAY[severity];
|
|
2095
|
+
console.log(`${display.color()}${display.symbol} ${severity.toUpperCase()} (${findings.length})${RESET()}`);
|
|
2096
|
+
for (const finding of findings) {
|
|
2097
|
+
console.log(` • [${finding.checkId}] ${finding.title}`);
|
|
2098
|
+
if (finding.port) {
|
|
2099
|
+
console.log(` Port: ${finding.port}${finding.path ? `, Path: ${finding.path}` : ''}`);
|
|
2100
|
+
}
|
|
2101
|
+
if (options.verbose) {
|
|
2102
|
+
console.log(` ${finding.description}`);
|
|
2103
|
+
console.log(` Evidence: ${finding.evidence}`);
|
|
2104
|
+
console.log(` Impact: ${finding.impact}`);
|
|
2105
|
+
console.log(` Fix: ${finding.fix}`);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
console.log();
|
|
2109
|
+
}
|
|
2110
|
+
// Exit with non-zero if critical/high issues found
|
|
2111
|
+
const criticalOrHigh = result.findings.filter((f) => f.severity === 'critical' || f.severity === 'high');
|
|
2112
|
+
if (criticalOrHigh.length > 0) {
|
|
2113
|
+
process.exit(1);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
catch (error) {
|
|
2117
|
+
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
2118
|
+
process.exit(1);
|
|
2119
|
+
}
|
|
2120
|
+
});
|
|
2121
|
+
program
|
|
2122
|
+
.command('rollback')
|
|
2123
|
+
.description(`Rollback auto-fix changes to the most recent backup
|
|
2124
|
+
|
|
2125
|
+
Restores files to their state before the last --fix operation.
|
|
2126
|
+
Backups are stored in .hackmyagent-backup/ with timestamps.
|
|
2127
|
+
|
|
2128
|
+
Examples:
|
|
2129
|
+
$ hackmyagent rollback Rollback current directory
|
|
2130
|
+
$ hackmyagent rollback ./my-project Rollback specific directory`)
|
|
2131
|
+
.argument('[directory]', 'Directory to rollback (defaults to current directory)', '.')
|
|
2132
|
+
.action(async (directory) => {
|
|
2133
|
+
try {
|
|
2134
|
+
const targetDir = directory.startsWith('/') ? directory : process.cwd() + '/' + directory;
|
|
2135
|
+
console.log(`\n🔄 Rolling back changes in ${targetDir}...\n`);
|
|
2136
|
+
const scanner = new index_1.HardeningScanner();
|
|
2137
|
+
await scanner.rollback(targetDir);
|
|
2138
|
+
console.log(`${colors.green}✅ Rollback successful!${RESET()}`);
|
|
2139
|
+
console.log(' All auto-fix changes have been reverted.\n');
|
|
2140
|
+
}
|
|
2141
|
+
catch (error) {
|
|
2142
|
+
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
2143
|
+
process.exit(1);
|
|
2144
|
+
}
|
|
2145
|
+
});
|
|
2146
|
+
// Attack command - adversarial security testing
|
|
2147
|
+
const ATTACK_CATEGORY_NAMES = Object.keys(index_1.ATTACK_CATEGORIES);
|
|
2148
|
+
program
|
|
2149
|
+
.command('attack')
|
|
2150
|
+
.description(`Adversarial security testing for AI agents
|
|
2151
|
+
|
|
2152
|
+
Red team your AI agent with ${index_1.PAYLOAD_STATS.total} attack payloads across 7 categories:
|
|
2153
|
+
• Prompt Injection: ${index_1.PAYLOAD_STATS.byCategory['prompt-injection']} payloads
|
|
2154
|
+
• Jailbreaking: ${index_1.PAYLOAD_STATS.byCategory['jailbreak']} payloads
|
|
2155
|
+
• Data Exfiltration: ${index_1.PAYLOAD_STATS.byCategory['data-exfiltration']} payloads
|
|
2156
|
+
• Capability Abuse: ${index_1.PAYLOAD_STATS.byCategory['capability-abuse']} payloads
|
|
2157
|
+
• Context Manipulation: ${index_1.PAYLOAD_STATS.byCategory['context-manipulation']} payloads
|
|
2158
|
+
• MCP Exploitation: ${index_1.PAYLOAD_STATS.byCategory['mcp-exploitation']} payloads
|
|
2159
|
+
• A2A Attacks: ${index_1.PAYLOAD_STATS.byCategory['a2a-attack']} payloads
|
|
2160
|
+
|
|
2161
|
+
Intensity levels (controls how many payloads run):
|
|
2162
|
+
passive Observation only (${index_1.PAYLOAD_STATS.byIntensity.passive} payloads)
|
|
2163
|
+
active Standard payloads (${index_1.PAYLOAD_STATS.byIntensity.passive + index_1.PAYLOAD_STATS.byIntensity.active} payloads, default)
|
|
2164
|
+
aggressive All payloads including creative/risky (${index_1.PAYLOAD_STATS.total} payloads)
|
|
2165
|
+
|
|
2166
|
+
Target types:
|
|
2167
|
+
api OpenAI/Anthropic chat completions (default)
|
|
2168
|
+
mcp MCP JSON-RPC server (tools/call, tools/list)
|
|
2169
|
+
a2a A2A agent messaging endpoint (/a2a/message)
|
|
2170
|
+
local Local simulation (no API calls)
|
|
2171
|
+
|
|
2172
|
+
Target types:
|
|
2173
|
+
api OpenAI/Anthropic chat completions (default)
|
|
2174
|
+
mcp MCP JSON-RPC server (tools/call, tools/list)
|
|
2175
|
+
a2a A2A agent messaging endpoint (/a2a/message)
|
|
2176
|
+
local Local simulation (no API calls)
|
|
2177
|
+
|
|
2178
|
+
Examples:
|
|
2179
|
+
$ hackmyagent attack https://api.example.com/v1/chat
|
|
2180
|
+
$ hackmyagent attack https://api.example.com --intensity aggressive
|
|
2181
|
+
$ hackmyagent attack https://api.example.com --category prompt-injection
|
|
2182
|
+
$ hackmyagent attack --local --system-prompt "You are a helpful assistant"
|
|
2183
|
+
$ hackmyagent attack https://api.example.com -f sarif -o results.sarif
|
|
2184
|
+
$ hackmyagent attack https://api.example.com --payload-file custom.json
|
|
2185
|
+
$ hackmyagent attack https://api.example.com --fail-on-vulnerable medium
|
|
2186
|
+
$ hackmyagent attack http://localhost:3010 --target-type mcp --category mcp-exploitation
|
|
2187
|
+
$ hackmyagent attack http://localhost:3020 --target-type a2a --category a2a-attack`)
|
|
2188
|
+
.argument('[target]', 'API endpoint to test (or use --local for simulation)')
|
|
2189
|
+
.option('-i, --intensity <level>', 'Attack intensity: passive, active, aggressive', 'active')
|
|
2190
|
+
.option('-c, --category <categories>', 'Comma-separated categories to test')
|
|
2191
|
+
.option('--local', 'Run in local simulation mode (no actual API calls)')
|
|
2192
|
+
.option('-t, --target-type <type>', 'Target type: api, mcp, a2a, local', 'api')
|
|
2193
|
+
.option('--api-format <format>', 'API format: openai, anthropic, mcp-jsonrpc, a2a, custom', 'openai')
|
|
2194
|
+
.option('--model <model>', 'Model to test (for API targets)')
|
|
2195
|
+
.option('--system-prompt <prompt>', 'System prompt (for local testing)')
|
|
2196
|
+
.option('--mcp-tool <tool>', 'Default MCP tool name (for mcp targets)')
|
|
2197
|
+
.option('--a2a-sender <name>', 'A2A sender identity (for a2a targets)', 'attacker-agent')
|
|
2198
|
+
.option('--a2a-recipient <name>', 'A2A recipient identity (for a2a targets)', 'target-agent')
|
|
2199
|
+
.option('-H, --header <headers>', 'Headers in format "Key: Value" (can be used multiple times)')
|
|
2200
|
+
.option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
|
|
2201
|
+
.option('--delay <ms>', 'Delay between requests in milliseconds', '1000')
|
|
2202
|
+
.option('--stop-on-success', 'Stop after first successful attack')
|
|
2203
|
+
.option('--payload-file <path>', 'JSON file with custom attack payloads')
|
|
2204
|
+
.option('--fail-on-vulnerable [severity]', 'Exit code 1 if vulnerabilities found (optional: critical/high/medium/low)')
|
|
2205
|
+
.option('-f, --format <format>', 'Output format: text, json, sarif, html', 'text')
|
|
2206
|
+
.option('-o, --output <file>', 'Write output to file')
|
|
2207
|
+
.option('-v, --verbose', 'Show detailed output for each payload')
|
|
2208
|
+
.option('--registry-report', 'Post results to OpenA2A Registry')
|
|
2209
|
+
.option('--no-registry', 'Skip auto-publishing results to OpenA2A Registry')
|
|
2210
|
+
.option('--version-id <id>', 'Registry version ID to report against')
|
|
2211
|
+
.option('--registry-url <url>', 'Registry URL (default: REGISTRY_URL env)')
|
|
2212
|
+
.option('--registry-key <key>', 'Registry API key (default: REGISTRY_API_KEY env)')
|
|
2213
|
+
.action(async (targetUrl, options) => {
|
|
2214
|
+
try {
|
|
2215
|
+
// Validate target
|
|
2216
|
+
if (!targetUrl && !options.local) {
|
|
2217
|
+
console.error('Error: Target URL required (or use --local for simulation)');
|
|
2218
|
+
process.exit(1);
|
|
2219
|
+
}
|
|
2220
|
+
// Validate intensity
|
|
2221
|
+
const validIntensities = ['passive', 'active', 'aggressive'];
|
|
2222
|
+
const intensity = (options.intensity || 'active');
|
|
2223
|
+
if (!validIntensities.includes(intensity)) {
|
|
2224
|
+
console.error(`Error: Invalid intensity '${options.intensity}'. Use: ${validIntensities.join(', ')}`);
|
|
2225
|
+
process.exit(1);
|
|
2226
|
+
}
|
|
2227
|
+
// Parse categories
|
|
2228
|
+
let categories;
|
|
2229
|
+
if (options.category) {
|
|
2230
|
+
categories = options.category.split(',').map(c => c.trim());
|
|
2231
|
+
for (const cat of categories) {
|
|
2232
|
+
if (!ATTACK_CATEGORY_NAMES.includes(cat)) {
|
|
2233
|
+
console.error(`Error: Invalid category '${cat}'. Use: ${ATTACK_CATEGORY_NAMES.join(', ')}`);
|
|
2234
|
+
process.exit(1);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
// Parse headers
|
|
2239
|
+
const headers = {};
|
|
2240
|
+
if (options.header) {
|
|
2241
|
+
const headerList = Array.isArray(options.header) ? options.header : [options.header];
|
|
2242
|
+
for (const h of headerList) {
|
|
2243
|
+
const [key, ...valueParts] = h.split(':');
|
|
2244
|
+
if (key && valueParts.length > 0) {
|
|
2245
|
+
headers[key.trim()] = valueParts.join(':').trim();
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
// Determine target type
|
|
2250
|
+
let targetType = 'api';
|
|
2251
|
+
if (options.local) {
|
|
2252
|
+
targetType = 'local';
|
|
2253
|
+
}
|
|
2254
|
+
else if (options.targetType) {
|
|
2255
|
+
const validTypes = ['api', 'mcp', 'a2a', 'local'];
|
|
2256
|
+
if (!validTypes.includes(options.targetType)) {
|
|
2257
|
+
console.error(`Error: Invalid target type '${options.targetType}'. Use: ${validTypes.join(', ')}`);
|
|
2258
|
+
process.exit(1);
|
|
2259
|
+
}
|
|
2260
|
+
targetType = options.targetType;
|
|
2261
|
+
}
|
|
2262
|
+
// Auto-detect api format from target type if not explicitly set
|
|
2263
|
+
let apiFormat = options.apiFormat || 'openai';
|
|
2264
|
+
if (targetType === 'mcp' && apiFormat === 'openai') {
|
|
2265
|
+
apiFormat = 'mcp-jsonrpc';
|
|
2266
|
+
}
|
|
2267
|
+
else if (targetType === 'a2a' && apiFormat === 'openai') {
|
|
2268
|
+
apiFormat = 'a2a';
|
|
2269
|
+
}
|
|
2270
|
+
// Build target
|
|
2271
|
+
const target = {
|
|
2272
|
+
url: targetUrl || '',
|
|
2273
|
+
type: targetType,
|
|
2274
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
2275
|
+
apiFormat: apiFormat,
|
|
2276
|
+
model: options.model,
|
|
2277
|
+
systemPrompt: options.systemPrompt,
|
|
2278
|
+
mcpTool: options.mcpTool,
|
|
2279
|
+
a2aSender: options.a2aSender,
|
|
2280
|
+
a2aRecipient: options.a2aRecipient,
|
|
2281
|
+
};
|
|
2282
|
+
// Validate format
|
|
2283
|
+
const validFormats = ['text', 'json', 'sarif', 'html'];
|
|
2284
|
+
const format = options.format || 'text';
|
|
2285
|
+
if (!validFormats.includes(format)) {
|
|
2286
|
+
console.error(`Error: Invalid format '${format}'. Use: ${validFormats.join(', ')}`);
|
|
2287
|
+
process.exit(1);
|
|
2288
|
+
}
|
|
2289
|
+
// Load custom payloads from file
|
|
2290
|
+
let customPayloads;
|
|
2291
|
+
if (options.payloadFile) {
|
|
2292
|
+
const filePath = require('path').resolve(options.payloadFile);
|
|
2293
|
+
if (!require('fs').existsSync(filePath)) {
|
|
2294
|
+
console.error(`Error: Payload file not found: ${filePath}`);
|
|
2295
|
+
process.exit(1);
|
|
2296
|
+
}
|
|
2297
|
+
const fileContent = require('fs').readFileSync(filePath, 'utf-8');
|
|
2298
|
+
customPayloads = (0, index_1.parseCustomPayloads)(fileContent);
|
|
2299
|
+
}
|
|
2300
|
+
// Show header for text output
|
|
2301
|
+
if (format === 'text') {
|
|
2302
|
+
console.log(`\n⚔️ HackMyAgent Attack Mode`);
|
|
2303
|
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
|
2304
|
+
console.log(`Target: ${target.type === 'local' ? 'Local Simulation' : targetUrl}`);
|
|
2305
|
+
console.log(`Intensity: ${intensity}`);
|
|
2306
|
+
if (customPayloads) {
|
|
2307
|
+
console.log(`Payloads: ${customPayloads.length} custom (from file)`);
|
|
2308
|
+
}
|
|
2309
|
+
else {
|
|
2310
|
+
console.log(`Categories: ${categories ? categories.join(', ') : 'all'}`);
|
|
2311
|
+
}
|
|
2312
|
+
console.log();
|
|
2313
|
+
}
|
|
2314
|
+
// Run attack
|
|
2315
|
+
const scanner = new index_1.AttackScanner();
|
|
2316
|
+
const report = await scanner.scan(target, {
|
|
2317
|
+
intensity,
|
|
2318
|
+
categories,
|
|
2319
|
+
customPayloads,
|
|
2320
|
+
timeout: parseInt(options.timeout || '30000', 10),
|
|
2321
|
+
delay: parseInt(options.delay || '1000', 10),
|
|
2322
|
+
stopOnSuccess: options.stopOnSuccess,
|
|
2323
|
+
verbose: options.verbose,
|
|
2324
|
+
});
|
|
2325
|
+
// Output results
|
|
2326
|
+
let output;
|
|
2327
|
+
switch (format) {
|
|
2328
|
+
case 'json':
|
|
2329
|
+
output = JSON.stringify(report, null, 2);
|
|
2330
|
+
break;
|
|
2331
|
+
case 'sarif':
|
|
2332
|
+
output = generateAttackSarif(report);
|
|
2333
|
+
break;
|
|
2334
|
+
case 'html':
|
|
2335
|
+
output = generateAttackHtmlReport(report);
|
|
2336
|
+
break;
|
|
2337
|
+
default: // text
|
|
2338
|
+
printAttackReport(report, options.verbose ?? false);
|
|
2339
|
+
output = '';
|
|
2340
|
+
}
|
|
2341
|
+
// Write output
|
|
2342
|
+
if (output) {
|
|
2343
|
+
if (options.output) {
|
|
2344
|
+
require('fs').writeFileSync(options.output, output);
|
|
2345
|
+
console.error(`Report written to ${options.output}`);
|
|
2346
|
+
}
|
|
2347
|
+
else {
|
|
2348
|
+
console.log(output);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
// Registry reporting: auto-publish to community endpoint by default
|
|
2352
|
+
const shouldReport = options.registryReport || (options.registry !== false);
|
|
2353
|
+
if (shouldReport) {
|
|
2354
|
+
try {
|
|
2355
|
+
const core = await Promise.resolve().then(() => __importStar(require('./index')));
|
|
2356
|
+
const registryUrl = options.registryUrl || process.env.REGISTRY_URL || 'https://registry.opena2a.org';
|
|
2357
|
+
if (options.versionId) {
|
|
2358
|
+
// Authenticated path: existing behavior (version-id + API key)
|
|
2359
|
+
const registryKey = options.registryKey || process.env.REGISTRY_API_KEY;
|
|
2360
|
+
if (!registryKey) {
|
|
2361
|
+
console.error('Error: --registry-key or REGISTRY_API_KEY env is required when using --version-id');
|
|
2362
|
+
process.exit(1);
|
|
2363
|
+
}
|
|
2364
|
+
const client = new core.RegistryClient({ registryUrl, apiKey: registryKey });
|
|
2365
|
+
const payload = core.buildAttackReport(options.versionId, report);
|
|
2366
|
+
await client.reportScanResult(payload);
|
|
2367
|
+
console.log(`Registry: attack results reported for version ${options.versionId}`);
|
|
2368
|
+
}
|
|
2369
|
+
else if (typeof core.buildCommunityAttackReport === 'function') {
|
|
2370
|
+
// Community path: request scan token, then submit results
|
|
2371
|
+
const client = new core.RegistryClient({ registryUrl, apiKey: '' });
|
|
2372
|
+
const packageName = target.url || targetUrl || 'unknown';
|
|
2373
|
+
const tokenResp = typeof client.requestScanToken === 'function'
|
|
2374
|
+
? await client.requestScanToken(packageName)
|
|
2375
|
+
: null;
|
|
2376
|
+
const payload = core.buildCommunityAttackReport(packageName, report);
|
|
2377
|
+
const resp = typeof client.reportCommunityResult === 'function'
|
|
2378
|
+
? await client.reportCommunityResult(payload, tokenResp?.scanToken)
|
|
2379
|
+
: { status: 'skipped' };
|
|
2380
|
+
if (resp.status === 'accepted') {
|
|
2381
|
+
console.log('Registry: attack results shared with OpenA2A community');
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
catch (reportErr) {
|
|
2386
|
+
console.error(`Registry: failed to report scan results: ${reportErr.message || reportErr}`);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
// Exit with non-zero based on fail policy
|
|
2390
|
+
if ((0, index_1.shouldFail)(report, options.failOnVulnerable)) {
|
|
2391
|
+
process.exit(1);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
catch (error) {
|
|
2395
|
+
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
2396
|
+
process.exit(1);
|
|
2397
|
+
}
|
|
2398
|
+
});
|
|
2399
|
+
// Attack report formatting
|
|
2400
|
+
function printAttackReport(report, verbose) {
|
|
2401
|
+
const riskColors = {
|
|
2402
|
+
'critical': colors.brightRed,
|
|
2403
|
+
'high': colors.red,
|
|
2404
|
+
'medium': colors.yellow,
|
|
2405
|
+
'low': colors.green,
|
|
2406
|
+
'secure': colors.green,
|
|
2407
|
+
};
|
|
2408
|
+
// Summary
|
|
2409
|
+
console.log(`Risk Score: ${riskColors[report.riskRating]}${report.riskScore}/100 (${report.riskRating.toUpperCase()})${RESET()}`);
|
|
2410
|
+
console.log(`Duration: ${report.duration}ms`);
|
|
2411
|
+
console.log();
|
|
2412
|
+
// Attack summary
|
|
2413
|
+
console.log(`Attacks: ${report.summary.total} total | ${colors.red}${report.summary.successful} successful${RESET()} | ${colors.green}${report.summary.blocked} blocked${RESET()} | ${report.summary.inconclusive} inconclusive`);
|
|
2414
|
+
console.log();
|
|
2415
|
+
// Category breakdown
|
|
2416
|
+
console.log(`Categories:`);
|
|
2417
|
+
for (const [cat, stats] of Object.entries(report.summary.byCategory)) {
|
|
2418
|
+
if (stats.total === 0)
|
|
2419
|
+
continue;
|
|
2420
|
+
const catInfo = index_1.ATTACK_CATEGORIES[cat];
|
|
2421
|
+
const icon = stats.successful > 0 ? '❌' : '✅';
|
|
2422
|
+
console.log(` ${icon} ${catInfo.name}: ${stats.successful}/${stats.total} successful`);
|
|
2423
|
+
}
|
|
2424
|
+
console.log();
|
|
2425
|
+
// Successful attacks
|
|
2426
|
+
const successful = report.results.filter(r => r.success);
|
|
2427
|
+
if (successful.length > 0) {
|
|
2428
|
+
console.log(`${colors.red}Successful Attacks:${RESET()}`);
|
|
2429
|
+
for (const r of successful) {
|
|
2430
|
+
const sevColor = r.payload.severity === 'critical' ? colors.brightRed :
|
|
2431
|
+
r.payload.severity === 'high' ? colors.red :
|
|
2432
|
+
r.payload.severity === 'medium' ? colors.yellow : colors.green;
|
|
2433
|
+
console.log(` ${sevColor}[${r.payload.severity.toUpperCase()}]${RESET()} ${r.payload.id}: ${r.payload.name}`);
|
|
2434
|
+
if (verbose) {
|
|
2435
|
+
console.log(` Evidence: ${r.evidence}`);
|
|
2436
|
+
console.log(` Remediation: ${r.payload.remediation}`);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
console.log();
|
|
2440
|
+
}
|
|
2441
|
+
// Blocked attacks (only in verbose)
|
|
2442
|
+
if (verbose) {
|
|
2443
|
+
const blocked = report.results.filter(r => r.blocked);
|
|
2444
|
+
if (blocked.length > 0) {
|
|
2445
|
+
console.log(`${colors.green}Blocked Attacks (${blocked.length}):${RESET()}`);
|
|
2446
|
+
for (const r of blocked) {
|
|
2447
|
+
console.log(` ✅ ${r.payload.id}: ${r.payload.name}`);
|
|
2448
|
+
}
|
|
2449
|
+
console.log();
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
2453
|
+
console.log(`\nUse --verbose for detailed attack results.`);
|
|
2454
|
+
console.log(`Use --intensity aggressive for advanced attacks.\n`);
|
|
2455
|
+
}
|
|
2456
|
+
// Generate SARIF output for attack results
|
|
2457
|
+
function generateAttackSarif(report) {
|
|
2458
|
+
const rules = report.results
|
|
2459
|
+
.filter(r => r.success)
|
|
2460
|
+
.map(r => ({
|
|
2461
|
+
id: r.payload.id,
|
|
2462
|
+
name: r.payload.name.replace(/\s+/g, ''),
|
|
2463
|
+
shortDescription: { text: r.payload.name },
|
|
2464
|
+
fullDescription: { text: r.payload.description },
|
|
2465
|
+
help: { text: r.payload.remediation },
|
|
2466
|
+
helpUri: `https://oasb.ai/attacks/${r.payload.id}`,
|
|
2467
|
+
defaultConfiguration: {
|
|
2468
|
+
level: r.payload.severity === 'critical' || r.payload.severity === 'high' ? 'error' :
|
|
2469
|
+
r.payload.severity === 'medium' ? 'warning' : 'note',
|
|
2470
|
+
},
|
|
2471
|
+
properties: {
|
|
2472
|
+
'security-severity': r.payload.severity === 'critical' ? '9.0' :
|
|
2473
|
+
r.payload.severity === 'high' ? '7.0' :
|
|
2474
|
+
r.payload.severity === 'medium' ? '5.0' : '3.0',
|
|
2475
|
+
tags: ['security', 'ai-agent', r.payload.category],
|
|
2476
|
+
},
|
|
2477
|
+
}));
|
|
2478
|
+
const results = report.results
|
|
2479
|
+
.filter(r => r.success)
|
|
2480
|
+
.map(r => ({
|
|
2481
|
+
ruleId: r.payload.id,
|
|
2482
|
+
level: r.payload.severity === 'critical' || r.payload.severity === 'high' ? 'error' :
|
|
2483
|
+
r.payload.severity === 'medium' ? 'warning' : 'note',
|
|
2484
|
+
message: { text: `${r.payload.name}: ${r.evidence}` },
|
|
2485
|
+
}));
|
|
2486
|
+
return JSON.stringify({
|
|
2487
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
2488
|
+
version: '2.1.0',
|
|
2489
|
+
runs: [{
|
|
2490
|
+
tool: {
|
|
2491
|
+
driver: {
|
|
2492
|
+
name: 'HackMyAgent',
|
|
2493
|
+
version: index_1.VERSION,
|
|
2494
|
+
informationUri: 'https://hackmyagent.com',
|
|
2495
|
+
rules,
|
|
2496
|
+
},
|
|
2497
|
+
},
|
|
2498
|
+
results,
|
|
2499
|
+
}],
|
|
2500
|
+
}, null, 2);
|
|
2501
|
+
}
|
|
2502
|
+
// Generate HTML report for attack results
|
|
2503
|
+
function generateAttackHtmlReport(report) {
|
|
2504
|
+
// Risk grade based on score
|
|
2505
|
+
const getGrade = (score) => {
|
|
2506
|
+
if (score <= 10)
|
|
2507
|
+
return { letter: 'A', color: '#22c55e' };
|
|
2508
|
+
if (score <= 25)
|
|
2509
|
+
return { letter: 'B', color: '#84cc16' };
|
|
2510
|
+
if (score <= 50)
|
|
2511
|
+
return { letter: 'C', color: '#eab308' };
|
|
2512
|
+
if (score <= 70)
|
|
2513
|
+
return { letter: 'D', color: '#f97316' };
|
|
2514
|
+
return { letter: 'F', color: '#ef4444' };
|
|
2515
|
+
};
|
|
2516
|
+
const grade = getGrade(report.riskScore);
|
|
2517
|
+
const ratingColor = {
|
|
2518
|
+
'critical': '#ef4444',
|
|
2519
|
+
'high': '#f97316',
|
|
2520
|
+
'medium': '#eab308',
|
|
2521
|
+
'low': '#22c55e',
|
|
2522
|
+
'secure': '#22c55e',
|
|
2523
|
+
};
|
|
2524
|
+
const ratingBg = {
|
|
2525
|
+
'critical': 'rgba(239, 68, 68, 0.15)',
|
|
2526
|
+
'high': 'rgba(249, 115, 22, 0.15)',
|
|
2527
|
+
'medium': 'rgba(234, 179, 8, 0.15)',
|
|
2528
|
+
'low': 'rgba(34, 197, 94, 0.15)',
|
|
2529
|
+
'secure': 'rgba(34, 197, 94, 0.15)',
|
|
2530
|
+
};
|
|
2531
|
+
// SVG icons
|
|
2532
|
+
const icons = {
|
|
2533
|
+
sword: '<svg class="icon icon-sword" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 17.5L3 6V3h3l11.5 11.5"/><path d="M13 19l6-6"/><path d="M16 16l4 4"/><path d="M19 21l2-2"/></svg>',
|
|
2534
|
+
shield: '<svg class="icon icon-shield" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clip-rule="evenodd"/></svg>',
|
|
2535
|
+
check: '<svg class="icon icon-check" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>',
|
|
2536
|
+
x: '<svg class="icon icon-x" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>',
|
|
2537
|
+
warning: '<svg class="icon icon-warning" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>',
|
|
2538
|
+
print: '<svg class="icon icon-print" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5 4v3H4a2 2 0 00-2 2v3a2 2 0 002 2h1v2a2 2 0 002 2h6a2 2 0 002-2v-2h1a2 2 0 002-2V9a2 2 0 00-2-2h-1V4a2 2 0 00-2-2H7a2 2 0 00-2 2zm8 0H7v3h6V4zm0 8H7v4h6v-4z" clip-rule="evenodd"/></svg>',
|
|
2539
|
+
};
|
|
2540
|
+
// Category abbreviations
|
|
2541
|
+
const categoryAbbrev = {
|
|
2542
|
+
'prompt-injection': 'PI',
|
|
2543
|
+
'jailbreak': 'JB',
|
|
2544
|
+
'data-exfiltration': 'DE',
|
|
2545
|
+
'capability-abuse': 'CA',
|
|
2546
|
+
'context-manipulation': 'CM',
|
|
2547
|
+
'mcp-exploitation': 'MCP',
|
|
2548
|
+
'a2a-attack': 'A2A',
|
|
2549
|
+
};
|
|
2550
|
+
// Donut chart for attack results
|
|
2551
|
+
const donutRadius = 60;
|
|
2552
|
+
const donutStroke = 12;
|
|
2553
|
+
const donutCircumference = 2 * Math.PI * donutRadius;
|
|
2554
|
+
const total = report.summary.total || 1;
|
|
2555
|
+
const successPct = report.summary.successful / total;
|
|
2556
|
+
const blockedPct = report.summary.blocked / total;
|
|
2557
|
+
const inconclusivePct = report.summary.inconclusive / total;
|
|
2558
|
+
const successDash = donutCircumference * successPct;
|
|
2559
|
+
const blockedDash = donutCircumference * blockedPct;
|
|
2560
|
+
const inconclusiveDash = donutCircumference * inconclusivePct;
|
|
2561
|
+
// Calculate offsets for each segment
|
|
2562
|
+
const successOffset = 0;
|
|
2563
|
+
const blockedOffset = successDash;
|
|
2564
|
+
const inconclusiveOffset = successDash + blockedDash;
|
|
2565
|
+
const donutSvg = `
|
|
2566
|
+
<svg width="160" height="160" viewBox="0 0 160 160">
|
|
2567
|
+
<!-- Background circle -->
|
|
2568
|
+
<circle cx="80" cy="80" r="${donutRadius}" fill="none" stroke="#334155" stroke-width="${donutStroke}"/>
|
|
2569
|
+
<!-- Inconclusive segment (gray) -->
|
|
2570
|
+
${inconclusivePct > 0 ? `<circle cx="80" cy="80" r="${donutRadius}" fill="none"
|
|
2571
|
+
stroke="#64748b" stroke-width="${donutStroke}"
|
|
2572
|
+
stroke-dasharray="${inconclusiveDash} ${donutCircumference}"
|
|
2573
|
+
stroke-dashoffset="${-inconclusiveOffset}"
|
|
2574
|
+
transform="rotate(-90 80 80)"/>` : ''}
|
|
2575
|
+
<!-- Blocked segment (green) -->
|
|
2576
|
+
${blockedPct > 0 ? `<circle cx="80" cy="80" r="${donutRadius}" fill="none"
|
|
2577
|
+
stroke="#22c55e" stroke-width="${donutStroke}"
|
|
2578
|
+
stroke-dasharray="${blockedDash} ${donutCircumference}"
|
|
2579
|
+
stroke-dashoffset="${-blockedOffset}"
|
|
2580
|
+
transform="rotate(-90 80 80)"/>` : ''}
|
|
2581
|
+
<!-- Successful segment (red) -->
|
|
2582
|
+
${successPct > 0 ? `<circle cx="80" cy="80" r="${donutRadius}" fill="none"
|
|
2583
|
+
stroke="#ef4444" stroke-width="${donutStroke}"
|
|
2584
|
+
stroke-dasharray="${successDash} ${donutCircumference}"
|
|
2585
|
+
stroke-dashoffset="${-successOffset}"
|
|
2586
|
+
transform="rotate(-90 80 80)"/>` : ''}
|
|
2587
|
+
<!-- Center text -->
|
|
2588
|
+
<text x="80" y="75" text-anchor="middle" fill="#f1f5f9" font-size="24" font-weight="700">${report.summary.total}</text>
|
|
2589
|
+
<text x="80" y="95" text-anchor="middle" fill="#94a3b8" font-size="12">attacks</text>
|
|
2590
|
+
</svg>`;
|
|
2591
|
+
// Generate category breakdown rows
|
|
2592
|
+
const categoryRows = Object.entries(report.summary.byCategory)
|
|
2593
|
+
.filter(([_, stats]) => stats.total > 0)
|
|
2594
|
+
.map(([cat, stats]) => {
|
|
2595
|
+
const catInfo = index_1.ATTACK_CATEGORIES[cat];
|
|
2596
|
+
const abbrev = categoryAbbrev[cat];
|
|
2597
|
+
const successRate = stats.total > 0 ? Math.round((stats.successful / stats.total) * 100) : 0;
|
|
2598
|
+
const barColor = stats.successful === 0 ? '#22c55e' : successRate > 50 ? '#ef4444' : '#eab308';
|
|
2599
|
+
const statusIcon = stats.successful === 0 ? icons.check : icons.x;
|
|
2600
|
+
const statusClass = stats.successful === 0 ? 'status-pass' : 'status-fail';
|
|
2601
|
+
// Get results for this category
|
|
2602
|
+
const catResults = report.results.filter(r => r.payload.category === cat);
|
|
2603
|
+
const resultRows = catResults.map(r => {
|
|
2604
|
+
const resultIcon = r.success ? icons.x : r.blocked ? icons.check : icons.warning;
|
|
2605
|
+
const resultClass = r.success ? 'status-fail' : r.blocked ? 'status-pass' : 'status-warn';
|
|
2606
|
+
const sevColor = r.payload.severity === 'critical' ? '#ef4444' :
|
|
2607
|
+
r.payload.severity === 'high' ? '#f97316' :
|
|
2608
|
+
r.payload.severity === 'medium' ? '#eab308' : '#22c55e';
|
|
2609
|
+
return `
|
|
2610
|
+
<tr class="attack-row ${r.success ? 'failed' : ''}">
|
|
2611
|
+
<td class="status-cell"><span class="${resultClass}">${resultIcon}</span></td>
|
|
2612
|
+
<td class="id-cell"><code>${r.payload.id}</code></td>
|
|
2613
|
+
<td class="name-cell">${escapeHtml(r.payload.name)}</td>
|
|
2614
|
+
<td class="severity-cell"><span class="severity-badge" style="color: ${sevColor}; background: ${sevColor}20;">${r.payload.severity.toUpperCase()}</span></td>
|
|
2615
|
+
<td class="result-cell">${r.success ? '<span class="result-tag fail">Succeeded</span>' : r.blocked ? '<span class="result-tag pass">Blocked</span>' : '<span class="result-tag warn">Inconclusive</span>'}</td>
|
|
2616
|
+
</tr>`;
|
|
2617
|
+
}).join('');
|
|
2618
|
+
return `
|
|
2619
|
+
<div class="category" id="cat-${abbrev}">
|
|
2620
|
+
<div class="category-header" onclick="toggleCategory('${abbrev}')">
|
|
2621
|
+
<span class="category-abbrev">[${abbrev}]</span>
|
|
2622
|
+
<span class="category-icon ${statusClass}">${statusIcon}</span>
|
|
2623
|
+
<span class="category-name">${escapeHtml(catInfo.name)}</span>
|
|
2624
|
+
<div class="category-meta">
|
|
2625
|
+
<span class="category-score">${stats.successful}/${stats.total} successful</span>
|
|
2626
|
+
<div class="mini-bar"><div class="mini-fill" style="width: ${successRate}%; background: ${barColor};"></div></div>
|
|
2627
|
+
<span class="chevron">▼</span>
|
|
2628
|
+
</div>
|
|
2629
|
+
</div>
|
|
2630
|
+
<div class="category-content">
|
|
2631
|
+
<table class="attacks-table">
|
|
2632
|
+
<thead><tr><th></th><th>ID</th><th>Attack</th><th>Severity</th><th>Result</th></tr></thead>
|
|
2633
|
+
<tbody>${resultRows}</tbody>
|
|
2634
|
+
</table>
|
|
2635
|
+
</div>
|
|
2636
|
+
</div>`;
|
|
2637
|
+
}).join('');
|
|
2638
|
+
// Successful attacks detail section
|
|
2639
|
+
const successfulAttacks = report.results.filter(r => r.success);
|
|
2640
|
+
const successfulDetailsHtml = successfulAttacks.length > 0 ? successfulAttacks.map(r => {
|
|
2641
|
+
const sevColor = r.payload.severity === 'critical' ? '#ef4444' :
|
|
2642
|
+
r.payload.severity === 'high' ? '#f97316' :
|
|
2643
|
+
r.payload.severity === 'medium' ? '#eab308' : '#22c55e';
|
|
2644
|
+
return `
|
|
2645
|
+
<div class="attack-detail">
|
|
2646
|
+
<div class="attack-detail-header">
|
|
2647
|
+
<code class="attack-id">${r.payload.id}</code>
|
|
2648
|
+
<span class="attack-name">${escapeHtml(r.payload.name)}</span>
|
|
2649
|
+
<span class="severity-badge" style="color: ${sevColor}; background: ${sevColor}20;">${r.payload.severity.toUpperCase()}</span>
|
|
2650
|
+
</div>
|
|
2651
|
+
<div class="attack-detail-meta">
|
|
2652
|
+
${r.payload.oasbControl ? `<span class="meta-tag">OASB ${r.payload.oasbControl}</span>` : ''}
|
|
2653
|
+
${r.payload.cwe ? `<span class="meta-tag">CWE-${r.payload.cwe}</span>` : ''}
|
|
2654
|
+
<span class="meta-tag">${index_1.ATTACK_CATEGORIES[r.payload.category].name}</span>
|
|
2655
|
+
</div>
|
|
2656
|
+
<div class="attack-detail-body">
|
|
2657
|
+
<div class="detail-section">
|
|
2658
|
+
<strong>Description:</strong> ${escapeHtml(r.payload.description)}
|
|
2659
|
+
</div>
|
|
2660
|
+
<div class="detail-section evidence">
|
|
2661
|
+
<strong>Evidence:</strong> ${escapeHtml(r.evidence)}
|
|
2662
|
+
</div>
|
|
2663
|
+
<div class="detail-section remediation">
|
|
2664
|
+
<strong>Remediation:</strong> ${escapeHtml(r.payload.remediation)}
|
|
2665
|
+
</div>
|
|
2666
|
+
</div>
|
|
2667
|
+
</div>`;
|
|
2668
|
+
}).join('') : '<div class="no-attacks">No successful attacks detected.</div>';
|
|
2669
|
+
return `<!DOCTYPE html>
|
|
2670
|
+
<html lang="en">
|
|
2671
|
+
<head>
|
|
2672
|
+
<meta charset="UTF-8">
|
|
2673
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2674
|
+
<title>HackMyAgent Attack Report | ${report.riskRating.toUpperCase()}</title>
|
|
2675
|
+
<style>
|
|
2676
|
+
:root {
|
|
2677
|
+
--bg-primary: #0a0f1a;
|
|
2678
|
+
--bg-secondary: #111827;
|
|
2679
|
+
--bg-tertiary: #1f2937;
|
|
2680
|
+
--text-primary: #f1f5f9;
|
|
2681
|
+
--text-secondary: #94a3b8;
|
|
2682
|
+
--text-muted: #64748b;
|
|
2683
|
+
--border: #334155;
|
|
2684
|
+
--accent: #3b82f6;
|
|
2685
|
+
--success: #22c55e;
|
|
2686
|
+
--warning: #eab308;
|
|
2687
|
+
--danger: #ef4444;
|
|
2688
|
+
}
|
|
2689
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2690
|
+
body {
|
|
2691
|
+
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2692
|
+
background: var(--bg-primary);
|
|
2693
|
+
color: var(--text-primary);
|
|
2694
|
+
line-height: 1.6;
|
|
2695
|
+
padding: 2rem;
|
|
2696
|
+
font-size: 14px;
|
|
2697
|
+
}
|
|
2698
|
+
.container { max-width: 1400px; margin: 0 auto; }
|
|
2699
|
+
|
|
2700
|
+
/* Header */
|
|
2701
|
+
.header {
|
|
2702
|
+
display: flex;
|
|
2703
|
+
justify-content: space-between;
|
|
2704
|
+
align-items: center;
|
|
2705
|
+
margin-bottom: 2rem;
|
|
2706
|
+
padding: 1.5rem 2rem;
|
|
2707
|
+
background: var(--bg-secondary);
|
|
2708
|
+
border-radius: 12px;
|
|
2709
|
+
border: 1px solid var(--border);
|
|
2710
|
+
}
|
|
2711
|
+
.header-left h1 {
|
|
2712
|
+
font-size: 1.5rem;
|
|
2713
|
+
font-weight: 700;
|
|
2714
|
+
display: flex;
|
|
2715
|
+
align-items: center;
|
|
2716
|
+
gap: 0.75rem;
|
|
2717
|
+
}
|
|
2718
|
+
.header-left .meta { color: var(--text-muted); font-size: 0.8rem; margin-top: 0.25rem; }
|
|
2719
|
+
.header-icon { display: inline-flex; margin-right: 0.5rem; }
|
|
2720
|
+
.header-icon .icon { width: 24px; height: 24px; color: var(--danger); }
|
|
2721
|
+
.header-right { display: flex; align-items: center; gap: 1rem; }
|
|
2722
|
+
.rating-badge {
|
|
2723
|
+
display: inline-block;
|
|
2724
|
+
padding: 0.375rem 1rem;
|
|
2725
|
+
border-radius: 6px;
|
|
2726
|
+
font-weight: 600;
|
|
2727
|
+
font-size: 0.875rem;
|
|
2728
|
+
background: ${ratingBg[report.riskRating]};
|
|
2729
|
+
color: ${ratingColor[report.riskRating]};
|
|
2730
|
+
border: 1px solid ${ratingColor[report.riskRating]}40;
|
|
2731
|
+
}
|
|
2732
|
+
.intensity-tag {
|
|
2733
|
+
display: inline-block;
|
|
2734
|
+
padding: 0.375rem 1rem;
|
|
2735
|
+
background: var(--accent);
|
|
2736
|
+
color: white;
|
|
2737
|
+
border-radius: 6px;
|
|
2738
|
+
font-size: 0.875rem;
|
|
2739
|
+
font-weight: 600;
|
|
2740
|
+
text-transform: capitalize;
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
/* SVG Icons */
|
|
2744
|
+
.icon { width: 16px; height: 16px; display: inline-block; vertical-align: middle; }
|
|
2745
|
+
.status-pass { color: var(--success); }
|
|
2746
|
+
.status-fail { color: var(--danger); }
|
|
2747
|
+
.status-warn { color: var(--warning); }
|
|
2748
|
+
.category-icon { display: flex; align-items: center; }
|
|
2749
|
+
.category-icon .icon { width: 18px; height: 18px; }
|
|
2750
|
+
.footer-btn .icon { width: 14px; height: 14px; margin-right: 0.375rem; }
|
|
2751
|
+
|
|
2752
|
+
/* Dashboard grid */
|
|
2753
|
+
.dashboard {
|
|
2754
|
+
display: grid;
|
|
2755
|
+
grid-template-columns: 280px 200px 1fr;
|
|
2756
|
+
gap: 1.5rem;
|
|
2757
|
+
margin-bottom: 2rem;
|
|
2758
|
+
}
|
|
2759
|
+
@media (max-width: 1200px) {
|
|
2760
|
+
.dashboard { grid-template-columns: 1fr 1fr; }
|
|
2761
|
+
.summary-section { grid-column: span 2; }
|
|
2762
|
+
}
|
|
2763
|
+
@media (max-width: 768px) {
|
|
2764
|
+
.dashboard { grid-template-columns: 1fr; }
|
|
2765
|
+
.summary-section { grid-column: span 1; }
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
/* Risk Score card */
|
|
2769
|
+
.score-card {
|
|
2770
|
+
background: var(--bg-secondary);
|
|
2771
|
+
border-radius: 12px;
|
|
2772
|
+
padding: 1.25rem;
|
|
2773
|
+
border: 1px solid var(--border);
|
|
2774
|
+
}
|
|
2775
|
+
.score-header {
|
|
2776
|
+
display: flex;
|
|
2777
|
+
align-items: center;
|
|
2778
|
+
gap: 1rem;
|
|
2779
|
+
margin-bottom: 1.25rem;
|
|
2780
|
+
padding-bottom: 1rem;
|
|
2781
|
+
border-bottom: 1px solid var(--border);
|
|
2782
|
+
}
|
|
2783
|
+
.score-grade {
|
|
2784
|
+
width: 56px;
|
|
2785
|
+
height: 56px;
|
|
2786
|
+
border-radius: 12px;
|
|
2787
|
+
border: 2px solid;
|
|
2788
|
+
display: flex;
|
|
2789
|
+
align-items: center;
|
|
2790
|
+
justify-content: center;
|
|
2791
|
+
}
|
|
2792
|
+
.grade-letter { font-size: 1.75rem; font-weight: 800; }
|
|
2793
|
+
.score-main { flex: 1; }
|
|
2794
|
+
.score-pct { font-size: 2rem; font-weight: 700; color: var(--text-primary); line-height: 1; }
|
|
2795
|
+
.score-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-top: 0.25rem; }
|
|
2796
|
+
|
|
2797
|
+
.score-stats { margin-top: 1rem; }
|
|
2798
|
+
.stat-row {
|
|
2799
|
+
display: flex;
|
|
2800
|
+
align-items: center;
|
|
2801
|
+
justify-content: space-between;
|
|
2802
|
+
padding: 0.5rem 0;
|
|
2803
|
+
border-bottom: 1px solid var(--border);
|
|
2804
|
+
}
|
|
2805
|
+
.stat-row:last-child { border-bottom: none; }
|
|
2806
|
+
.stat-label { color: var(--text-secondary); font-size: 0.85rem; }
|
|
2807
|
+
.stat-value { font-weight: 600; }
|
|
2808
|
+
.stat-value.danger { color: var(--danger); }
|
|
2809
|
+
.stat-value.success { color: var(--success); }
|
|
2810
|
+
.stat-value.muted { color: var(--text-muted); }
|
|
2811
|
+
|
|
2812
|
+
/* Donut chart section */
|
|
2813
|
+
.donut-section {
|
|
2814
|
+
background: var(--bg-secondary);
|
|
2815
|
+
border-radius: 12px;
|
|
2816
|
+
padding: 1.25rem;
|
|
2817
|
+
border: 1px solid var(--border);
|
|
2818
|
+
display: flex;
|
|
2819
|
+
flex-direction: column;
|
|
2820
|
+
align-items: center;
|
|
2821
|
+
}
|
|
2822
|
+
.donut-section h3 {
|
|
2823
|
+
font-size: 0.85rem;
|
|
2824
|
+
color: var(--text-secondary);
|
|
2825
|
+
text-transform: uppercase;
|
|
2826
|
+
letter-spacing: 0.05em;
|
|
2827
|
+
margin-bottom: 1rem;
|
|
2828
|
+
width: 100%;
|
|
2829
|
+
}
|
|
2830
|
+
.donut-legend {
|
|
2831
|
+
display: flex;
|
|
2832
|
+
flex-direction: column;
|
|
2833
|
+
gap: 0.5rem;
|
|
2834
|
+
margin-top: 1rem;
|
|
2835
|
+
width: 100%;
|
|
2836
|
+
}
|
|
2837
|
+
.legend-item {
|
|
2838
|
+
display: flex;
|
|
2839
|
+
align-items: center;
|
|
2840
|
+
gap: 0.5rem;
|
|
2841
|
+
font-size: 0.8rem;
|
|
2842
|
+
color: var(--text-secondary);
|
|
2843
|
+
}
|
|
2844
|
+
.legend-dot {
|
|
2845
|
+
width: 10px;
|
|
2846
|
+
height: 10px;
|
|
2847
|
+
border-radius: 50%;
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
/* Summary section */
|
|
2851
|
+
.summary-section {
|
|
2852
|
+
background: var(--bg-secondary);
|
|
2853
|
+
border-radius: 12px;
|
|
2854
|
+
padding: 1.5rem;
|
|
2855
|
+
border: 1px solid var(--border);
|
|
2856
|
+
}
|
|
2857
|
+
.summary-section h3 {
|
|
2858
|
+
font-size: 0.85rem;
|
|
2859
|
+
color: var(--text-secondary);
|
|
2860
|
+
text-transform: uppercase;
|
|
2861
|
+
letter-spacing: 0.05em;
|
|
2862
|
+
margin-bottom: 1rem;
|
|
2863
|
+
}
|
|
2864
|
+
.severity-breakdown {
|
|
2865
|
+
display: flex;
|
|
2866
|
+
gap: 1rem;
|
|
2867
|
+
flex-wrap: wrap;
|
|
2868
|
+
}
|
|
2869
|
+
.severity-item {
|
|
2870
|
+
display: flex;
|
|
2871
|
+
align-items: center;
|
|
2872
|
+
gap: 0.5rem;
|
|
2873
|
+
padding: 0.5rem 1rem;
|
|
2874
|
+
background: var(--bg-tertiary);
|
|
2875
|
+
border-radius: 6px;
|
|
2876
|
+
}
|
|
2877
|
+
.severity-count { font-size: 1.25rem; font-weight: 700; }
|
|
2878
|
+
.severity-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; }
|
|
2879
|
+
|
|
2880
|
+
/* Categories */
|
|
2881
|
+
.categories-section {
|
|
2882
|
+
margin-bottom: 2rem;
|
|
2883
|
+
}
|
|
2884
|
+
.categories-header {
|
|
2885
|
+
display: flex;
|
|
2886
|
+
justify-content: space-between;
|
|
2887
|
+
align-items: center;
|
|
2888
|
+
margin-bottom: 1rem;
|
|
2889
|
+
}
|
|
2890
|
+
.categories-header h2 { font-size: 1.1rem; }
|
|
2891
|
+
.expand-all {
|
|
2892
|
+
background: var(--bg-tertiary);
|
|
2893
|
+
border: 1px solid var(--border);
|
|
2894
|
+
color: var(--text-secondary);
|
|
2895
|
+
padding: 0.5rem 1rem;
|
|
2896
|
+
border-radius: 6px;
|
|
2897
|
+
cursor: pointer;
|
|
2898
|
+
font-size: 0.8rem;
|
|
2899
|
+
}
|
|
2900
|
+
.expand-all:hover { background: var(--border); }
|
|
2901
|
+
|
|
2902
|
+
.category {
|
|
2903
|
+
background: var(--bg-secondary);
|
|
2904
|
+
border-radius: 8px;
|
|
2905
|
+
margin-bottom: 0.75rem;
|
|
2906
|
+
border: 1px solid var(--border);
|
|
2907
|
+
overflow: hidden;
|
|
2908
|
+
}
|
|
2909
|
+
.category-header {
|
|
2910
|
+
display: flex;
|
|
2911
|
+
align-items: center;
|
|
2912
|
+
gap: 0.75rem;
|
|
2913
|
+
padding: 1rem 1.25rem;
|
|
2914
|
+
cursor: pointer;
|
|
2915
|
+
transition: background 0.15s;
|
|
2916
|
+
}
|
|
2917
|
+
.category-header:hover { background: var(--bg-tertiary); }
|
|
2918
|
+
.category-abbrev {
|
|
2919
|
+
font-family: monospace;
|
|
2920
|
+
font-size: 0.85rem;
|
|
2921
|
+
color: var(--accent);
|
|
2922
|
+
font-weight: 600;
|
|
2923
|
+
}
|
|
2924
|
+
.category-icon { font-size: 1.1rem; }
|
|
2925
|
+
.category-name { flex: 1; font-weight: 500; }
|
|
2926
|
+
.category-meta { display: flex; align-items: center; gap: 0.75rem; }
|
|
2927
|
+
.category-score { color: var(--text-secondary); font-size: 0.85rem; font-weight: 500; }
|
|
2928
|
+
.mini-bar { width: 60px; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
|
|
2929
|
+
.mini-fill { height: 100%; border-radius: 3px; }
|
|
2930
|
+
.chevron {
|
|
2931
|
+
color: var(--text-muted);
|
|
2932
|
+
font-size: 0.7rem;
|
|
2933
|
+
transition: transform 0.2s;
|
|
2934
|
+
margin-left: 0.5rem;
|
|
2935
|
+
}
|
|
2936
|
+
.category.collapsed .chevron { transform: rotate(-90deg); }
|
|
2937
|
+
.category.collapsed .category-content { display: none; }
|
|
2938
|
+
|
|
2939
|
+
.category-content { border-top: 1px solid var(--border); }
|
|
2940
|
+
.attacks-table { width: 100%; border-collapse: collapse; }
|
|
2941
|
+
.attacks-table th {
|
|
2942
|
+
padding: 0.75rem 1rem;
|
|
2943
|
+
text-align: left;
|
|
2944
|
+
background: var(--bg-primary);
|
|
2945
|
+
color: var(--text-muted);
|
|
2946
|
+
font-weight: 500;
|
|
2947
|
+
font-size: 0.75rem;
|
|
2948
|
+
text-transform: uppercase;
|
|
2949
|
+
letter-spacing: 0.03em;
|
|
2950
|
+
}
|
|
2951
|
+
.attacks-table td {
|
|
2952
|
+
padding: 0.875rem 1rem;
|
|
2953
|
+
border-top: 1px solid var(--border);
|
|
2954
|
+
vertical-align: middle;
|
|
2955
|
+
}
|
|
2956
|
+
.status-cell { width: 40px; text-align: center; }
|
|
2957
|
+
.id-cell { width: 80px; }
|
|
2958
|
+
.id-cell code {
|
|
2959
|
+
background: var(--bg-tertiary);
|
|
2960
|
+
padding: 0.2rem 0.5rem;
|
|
2961
|
+
border-radius: 4px;
|
|
2962
|
+
font-size: 0.8rem;
|
|
2963
|
+
color: var(--accent);
|
|
2964
|
+
}
|
|
2965
|
+
.name-cell { width: 40%; }
|
|
2966
|
+
.severity-cell { width: 80px; }
|
|
2967
|
+
.result-cell { width: 100px; }
|
|
2968
|
+
.attack-row.failed { background: rgba(239, 68, 68, 0.05); }
|
|
2969
|
+
|
|
2970
|
+
.severity-badge {
|
|
2971
|
+
padding: 0.2rem 0.6rem;
|
|
2972
|
+
border-radius: 4px;
|
|
2973
|
+
font-size: 0.7rem;
|
|
2974
|
+
font-weight: 600;
|
|
2975
|
+
text-transform: uppercase;
|
|
2976
|
+
}
|
|
2977
|
+
.result-tag {
|
|
2978
|
+
padding: 0.2rem 0.6rem;
|
|
2979
|
+
border-radius: 4px;
|
|
2980
|
+
font-size: 0.7rem;
|
|
2981
|
+
font-weight: 600;
|
|
2982
|
+
}
|
|
2983
|
+
.result-tag.pass { background: rgba(34, 197, 94, 0.2); color: var(--success); }
|
|
2984
|
+
.result-tag.fail { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
|
|
2985
|
+
.result-tag.warn { background: rgba(234, 179, 8, 0.2); color: var(--warning); }
|
|
2986
|
+
|
|
2987
|
+
/* Successful attacks detail */
|
|
2988
|
+
.details-section {
|
|
2989
|
+
margin-bottom: 2rem;
|
|
2990
|
+
}
|
|
2991
|
+
.details-section h2 {
|
|
2992
|
+
font-size: 1.1rem;
|
|
2993
|
+
margin-bottom: 1rem;
|
|
2994
|
+
display: flex;
|
|
2995
|
+
align-items: center;
|
|
2996
|
+
gap: 0.5rem;
|
|
2997
|
+
}
|
|
2998
|
+
.details-section h2 .icon { color: var(--danger); }
|
|
2999
|
+
.attack-detail {
|
|
3000
|
+
background: var(--bg-secondary);
|
|
3001
|
+
border-radius: 8px;
|
|
3002
|
+
margin-bottom: 1rem;
|
|
3003
|
+
border: 1px solid var(--border);
|
|
3004
|
+
border-left: 3px solid var(--danger);
|
|
3005
|
+
overflow: hidden;
|
|
3006
|
+
}
|
|
3007
|
+
.attack-detail-header {
|
|
3008
|
+
display: flex;
|
|
3009
|
+
align-items: center;
|
|
3010
|
+
gap: 1rem;
|
|
3011
|
+
padding: 1rem 1.25rem;
|
|
3012
|
+
background: rgba(239, 68, 68, 0.05);
|
|
3013
|
+
}
|
|
3014
|
+
.attack-id {
|
|
3015
|
+
background: var(--bg-tertiary);
|
|
3016
|
+
padding: 0.2rem 0.5rem;
|
|
3017
|
+
border-radius: 4px;
|
|
3018
|
+
font-size: 0.85rem;
|
|
3019
|
+
color: var(--danger);
|
|
3020
|
+
}
|
|
3021
|
+
.attack-name { flex: 1; font-weight: 500; }
|
|
3022
|
+
.attack-detail-meta {
|
|
3023
|
+
display: flex;
|
|
3024
|
+
gap: 0.5rem;
|
|
3025
|
+
padding: 0.75rem 1.25rem;
|
|
3026
|
+
background: var(--bg-tertiary);
|
|
3027
|
+
border-bottom: 1px solid var(--border);
|
|
3028
|
+
}
|
|
3029
|
+
.meta-tag {
|
|
3030
|
+
padding: 0.2rem 0.5rem;
|
|
3031
|
+
background: var(--bg-secondary);
|
|
3032
|
+
border-radius: 4px;
|
|
3033
|
+
font-size: 0.75rem;
|
|
3034
|
+
color: var(--text-muted);
|
|
3035
|
+
}
|
|
3036
|
+
.attack-detail-body { padding: 1rem 1.25rem; }
|
|
3037
|
+
.detail-section {
|
|
3038
|
+
margin-bottom: 0.75rem;
|
|
3039
|
+
font-size: 0.9rem;
|
|
3040
|
+
color: var(--text-secondary);
|
|
3041
|
+
}
|
|
3042
|
+
.detail-section:last-child { margin-bottom: 0; }
|
|
3043
|
+
.detail-section strong { color: var(--text-primary); margin-right: 0.5rem; }
|
|
3044
|
+
.detail-section.evidence {
|
|
3045
|
+
padding: 0.75rem;
|
|
3046
|
+
background: rgba(239, 68, 68, 0.1);
|
|
3047
|
+
border-radius: 6px;
|
|
3048
|
+
border-left: 3px solid var(--danger);
|
|
3049
|
+
}
|
|
3050
|
+
.detail-section.remediation {
|
|
3051
|
+
padding: 0.75rem;
|
|
3052
|
+
background: var(--bg-tertiary);
|
|
3053
|
+
border-radius: 6px;
|
|
3054
|
+
border-left: 3px solid var(--accent);
|
|
3055
|
+
}
|
|
3056
|
+
.no-attacks {
|
|
3057
|
+
padding: 2rem;
|
|
3058
|
+
text-align: center;
|
|
3059
|
+
color: var(--success);
|
|
3060
|
+
background: var(--bg-secondary);
|
|
3061
|
+
border-radius: 8px;
|
|
3062
|
+
border: 1px solid var(--border);
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
/* Footer */
|
|
3066
|
+
.footer {
|
|
3067
|
+
display: flex;
|
|
3068
|
+
justify-content: space-between;
|
|
3069
|
+
align-items: center;
|
|
3070
|
+
margin-top: 2rem;
|
|
3071
|
+
padding: 1.5rem;
|
|
3072
|
+
background: var(--bg-secondary);
|
|
3073
|
+
border-radius: 12px;
|
|
3074
|
+
border: 1px solid var(--border);
|
|
3075
|
+
color: var(--text-muted);
|
|
3076
|
+
font-size: 0.85rem;
|
|
3077
|
+
}
|
|
3078
|
+
.footer a { color: var(--accent); text-decoration: none; }
|
|
3079
|
+
.footer a:hover { text-decoration: underline; }
|
|
3080
|
+
.footer-actions { display: flex; gap: 1rem; }
|
|
3081
|
+
.footer-btn {
|
|
3082
|
+
display: flex;
|
|
3083
|
+
align-items: center;
|
|
3084
|
+
padding: 0.5rem 1rem;
|
|
3085
|
+
background: var(--bg-tertiary);
|
|
3086
|
+
border: 1px solid var(--border);
|
|
3087
|
+
border-radius: 6px;
|
|
3088
|
+
color: var(--text-primary);
|
|
3089
|
+
cursor: pointer;
|
|
3090
|
+
font-size: 0.8rem;
|
|
3091
|
+
}
|
|
3092
|
+
.footer-btn:hover { background: var(--border); }
|
|
3093
|
+
|
|
3094
|
+
/* Print styles */
|
|
3095
|
+
@media print {
|
|
3096
|
+
body { background: white; color: black; padding: 1rem; }
|
|
3097
|
+
.container { max-width: 100%; }
|
|
3098
|
+
.header, .score-card, .donut-section, .summary-section, .category, .attack-detail, .footer {
|
|
3099
|
+
background: white;
|
|
3100
|
+
border: 1px solid #ddd;
|
|
3101
|
+
break-inside: avoid;
|
|
3102
|
+
}
|
|
3103
|
+
.category.collapsed .category-content { display: block !important; }
|
|
3104
|
+
.chevron, .expand-all, .footer-actions { display: none; }
|
|
3105
|
+
.category-header { cursor: default; }
|
|
3106
|
+
.attack-row.failed { background: #fff0f0; }
|
|
3107
|
+
:root {
|
|
3108
|
+
--bg-primary: white;
|
|
3109
|
+
--bg-secondary: white;
|
|
3110
|
+
--bg-tertiary: #f5f5f5;
|
|
3111
|
+
--text-primary: black;
|
|
3112
|
+
--text-secondary: #555;
|
|
3113
|
+
--text-muted: #888;
|
|
3114
|
+
--border: #ddd;
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
</style>
|
|
3118
|
+
</head>
|
|
3119
|
+
<body>
|
|
3120
|
+
<div class="container">
|
|
3121
|
+
<header class="header">
|
|
3122
|
+
<div class="header-left">
|
|
3123
|
+
<h1><span class="header-icon">${icons.sword}</span>HackMyAgent Attack Report</h1>
|
|
3124
|
+
<div class="meta">Target: ${escapeHtml(report.target || 'Local Simulation')} • ${new Date(report.endTime).toLocaleString()}</div>
|
|
3125
|
+
</div>
|
|
3126
|
+
<div class="header-right">
|
|
3127
|
+
<div class="rating-badge">${report.riskRating.toUpperCase()} RISK</div>
|
|
3128
|
+
<div class="intensity-tag">${report.intensity}</div>
|
|
3129
|
+
</div>
|
|
3130
|
+
</header>
|
|
3131
|
+
|
|
3132
|
+
<div class="dashboard">
|
|
3133
|
+
<div class="score-card">
|
|
3134
|
+
<div class="score-header">
|
|
3135
|
+
<div class="score-grade" style="background: ${grade.color}20; border-color: ${grade.color};">
|
|
3136
|
+
<span class="grade-letter" style="color: ${grade.color};">${grade.letter}</span>
|
|
3137
|
+
</div>
|
|
3138
|
+
<div class="score-main">
|
|
3139
|
+
<div class="score-pct">${report.riskScore}/100</div>
|
|
3140
|
+
<div class="score-label">Risk Score</div>
|
|
3141
|
+
</div>
|
|
3142
|
+
</div>
|
|
3143
|
+
<div class="score-stats">
|
|
3144
|
+
<div class="stat-row">
|
|
3145
|
+
<span class="stat-label">Successful Attacks</span>
|
|
3146
|
+
<span class="stat-value danger">${report.summary.successful}</span>
|
|
3147
|
+
</div>
|
|
3148
|
+
<div class="stat-row">
|
|
3149
|
+
<span class="stat-label">Blocked Attacks</span>
|
|
3150
|
+
<span class="stat-value success">${report.summary.blocked}</span>
|
|
3151
|
+
</div>
|
|
3152
|
+
<div class="stat-row">
|
|
3153
|
+
<span class="stat-label">Inconclusive</span>
|
|
3154
|
+
<span class="stat-value muted">${report.summary.inconclusive}</span>
|
|
3155
|
+
</div>
|
|
3156
|
+
<div class="stat-row">
|
|
3157
|
+
<span class="stat-label">Duration</span>
|
|
3158
|
+
<span class="stat-value">${report.duration}ms</span>
|
|
3159
|
+
</div>
|
|
3160
|
+
</div>
|
|
3161
|
+
</div>
|
|
3162
|
+
|
|
3163
|
+
<div class="donut-section">
|
|
3164
|
+
<h3>Attack Results</h3>
|
|
3165
|
+
${donutSvg}
|
|
3166
|
+
<div class="donut-legend">
|
|
3167
|
+
<div class="legend-item"><span class="legend-dot" style="background: #ef4444;"></span> Successful (${report.summary.successful})</div>
|
|
3168
|
+
<div class="legend-item"><span class="legend-dot" style="background: #22c55e;"></span> Blocked (${report.summary.blocked})</div>
|
|
3169
|
+
<div class="legend-item"><span class="legend-dot" style="background: #64748b;"></span> Inconclusive (${report.summary.inconclusive})</div>
|
|
3170
|
+
</div>
|
|
3171
|
+
</div>
|
|
3172
|
+
|
|
3173
|
+
<div class="summary-section">
|
|
3174
|
+
<h3>Severity Breakdown (Successful Attacks)</h3>
|
|
3175
|
+
<div class="severity-breakdown">
|
|
3176
|
+
<div class="severity-item">
|
|
3177
|
+
<span class="severity-count" style="color: #ef4444;">${report.summary.bySeverity.critical || 0}</span>
|
|
3178
|
+
<span class="severity-label">Critical</span>
|
|
3179
|
+
</div>
|
|
3180
|
+
<div class="severity-item">
|
|
3181
|
+
<span class="severity-count" style="color: #f97316;">${report.summary.bySeverity.high || 0}</span>
|
|
3182
|
+
<span class="severity-label">High</span>
|
|
3183
|
+
</div>
|
|
3184
|
+
<div class="severity-item">
|
|
3185
|
+
<span class="severity-count" style="color: #eab308;">${report.summary.bySeverity.medium || 0}</span>
|
|
3186
|
+
<span class="severity-label">Medium</span>
|
|
3187
|
+
</div>
|
|
3188
|
+
<div class="severity-item">
|
|
3189
|
+
<span class="severity-count" style="color: #22c55e;">${report.summary.bySeverity.low || 0}</span>
|
|
3190
|
+
<span class="severity-label">Low</span>
|
|
3191
|
+
</div>
|
|
3192
|
+
</div>
|
|
3193
|
+
</div>
|
|
3194
|
+
</div>
|
|
3195
|
+
|
|
3196
|
+
<div class="categories-section">
|
|
3197
|
+
<div class="categories-header">
|
|
3198
|
+
<h2>Category Breakdown</h2>
|
|
3199
|
+
<button class="expand-all" onclick="toggleAll()">Expand/Collapse All</button>
|
|
3200
|
+
</div>
|
|
3201
|
+
${categoryRows}
|
|
3202
|
+
</div>
|
|
3203
|
+
|
|
3204
|
+
<div class="details-section">
|
|
3205
|
+
<h2>${icons.x} Successful Attacks Detail</h2>
|
|
3206
|
+
${successfulDetailsHtml}
|
|
3207
|
+
</div>
|
|
3208
|
+
|
|
3209
|
+
<footer class="footer">
|
|
3210
|
+
<div>Generated by <a href="https://hackmyagent.com">HackMyAgent</a> v${index_1.VERSION} • <a href="https://oasb.ai/attacks">oasb.ai/attacks</a></div>
|
|
3211
|
+
<div class="footer-actions">
|
|
3212
|
+
<button class="footer-btn" onclick="window.print()">${icons.print} Print Report</button>
|
|
3213
|
+
</div>
|
|
3214
|
+
</footer>
|
|
3215
|
+
</div>
|
|
3216
|
+
|
|
3217
|
+
<script>
|
|
3218
|
+
function toggleCategory(id) {
|
|
3219
|
+
const cat = document.getElementById('cat-' + id);
|
|
3220
|
+
cat.classList.toggle('collapsed');
|
|
3221
|
+
}
|
|
3222
|
+
function toggleAll() {
|
|
3223
|
+
const cats = document.querySelectorAll('.category');
|
|
3224
|
+
const allCollapsed = Array.from(cats).every(c => c.classList.contains('collapsed'));
|
|
3225
|
+
cats.forEach(c => {
|
|
3226
|
+
if (allCollapsed) {
|
|
3227
|
+
c.classList.remove('collapsed');
|
|
3228
|
+
} else {
|
|
3229
|
+
c.classList.add('collapsed');
|
|
3230
|
+
}
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
3233
|
+
</script>
|
|
3234
|
+
</body>
|
|
3235
|
+
</html>`;
|
|
3236
|
+
}
|
|
3237
|
+
// --- fix-all: Run all OpenClaw plugins to scan and remediate ---
|
|
3238
|
+
const credvault_1 = require("./plugins/credvault");
|
|
3239
|
+
const signcrypt_1 = require("./plugins/signcrypt");
|
|
3240
|
+
const skillguard_1 = require("./plugins/skillguard");
|
|
3241
|
+
const aim_core_1 = require("@opena2a/aim-core");
|
|
3242
|
+
const PLUGIN_SEVERITY_DISPLAY = {
|
|
3243
|
+
critical: { symbol: '[!!]', color: () => colors.brightRed },
|
|
3244
|
+
high: { symbol: '[!]', color: () => colors.red },
|
|
3245
|
+
medium: { symbol: '[~]', color: () => colors.yellow },
|
|
3246
|
+
low: { symbol: '[.]', color: () => colors.green },
|
|
3247
|
+
info: { symbol: '[i]', color: () => colors.cyan },
|
|
3248
|
+
};
|
|
3249
|
+
program
|
|
3250
|
+
.command('fix-all')
|
|
3251
|
+
.description(`Run all OpenA2A security plugins to scan and auto-fix agent issues
|
|
3252
|
+
|
|
3253
|
+
Runs the full plugin suite in order:
|
|
3254
|
+
1. SkillGuard — hash pinning, tamper detection, dangerous patterns
|
|
3255
|
+
2. SignCrypt — Ed25519 signing, heartbeat hash pins
|
|
3256
|
+
3. CredVault — credential detection, env var replacement
|
|
3257
|
+
|
|
3258
|
+
Each plugin scans for findings, then auto-fixes what it can.
|
|
3259
|
+
Dangerous patterns (reverse shells, exfil, etc.) require manual review.
|
|
3260
|
+
|
|
3261
|
+
Exit code 1 if critical/high issues remain after fixing.
|
|
3262
|
+
|
|
3263
|
+
Examples:
|
|
3264
|
+
$ hackmyagent fix-all Scan and fix current directory
|
|
3265
|
+
$ hackmyagent fix-all ./my-agent Scan specific directory
|
|
3266
|
+
$ hackmyagent fix-all --dry-run Preview fixes without applying
|
|
3267
|
+
$ hackmyagent fix-all --scan-only Scan without fixing
|
|
3268
|
+
$ hackmyagent fix-all --json JSON output for CI
|
|
3269
|
+
$ hackmyagent fix-all --with-aim Enable AIM identity and audit`)
|
|
3270
|
+
.argument('[directory]', 'Agent directory to scan (default: current directory)', '')
|
|
3271
|
+
.option('--dry-run', 'Preview fixes without applying them')
|
|
3272
|
+
.option('--scan-only', 'Only scan, do not fix')
|
|
3273
|
+
.option('--json', 'Output as JSON (for scripting/CI)')
|
|
3274
|
+
.option('--with-aim', 'Initialize AIM Core for identity-aware audit logging')
|
|
3275
|
+
.option('-v, --verbose', 'Show all findings including passed plugins')
|
|
3276
|
+
.action(async (directory, options) => {
|
|
3277
|
+
try {
|
|
3278
|
+
const path = require('path');
|
|
3279
|
+
const fs = require('fs');
|
|
3280
|
+
// Resolve target directory with symlink protection
|
|
3281
|
+
let targetDir;
|
|
3282
|
+
if (directory && directory !== '') {
|
|
3283
|
+
targetDir = path.isAbsolute(directory) ? directory : path.resolve(process.cwd(), directory);
|
|
3284
|
+
}
|
|
3285
|
+
else {
|
|
3286
|
+
targetDir = process.cwd();
|
|
3287
|
+
}
|
|
3288
|
+
// Resolve realpath atomically — eliminates TOCTOU between existence check and resolution
|
|
3289
|
+
let realTarget;
|
|
3290
|
+
try {
|
|
3291
|
+
realTarget = fs.realpathSync(targetDir);
|
|
3292
|
+
}
|
|
3293
|
+
catch {
|
|
3294
|
+
console.error(`Error: Directory not found: ${targetDir}`);
|
|
3295
|
+
process.exit(1);
|
|
3296
|
+
}
|
|
3297
|
+
// Verify resolved path is a directory (realpath already resolved any symlinks)
|
|
3298
|
+
const resolvedStat = fs.statSync(realTarget);
|
|
3299
|
+
if (!resolvedStat.isDirectory()) {
|
|
3300
|
+
console.error(`Error: Not a directory: ${realTarget}`);
|
|
3301
|
+
process.exit(1);
|
|
3302
|
+
}
|
|
3303
|
+
// Block path traversal via .. in relative paths (but allow absolute paths)
|
|
3304
|
+
if (!path.isAbsolute(directory) && directory && directory !== '') {
|
|
3305
|
+
const realCwd = fs.realpathSync(process.cwd());
|
|
3306
|
+
const relative = path.relative(realCwd, realTarget);
|
|
3307
|
+
if (relative.startsWith('..')) {
|
|
3308
|
+
console.error(`Error: Target directory must not traverse above current working directory. Use an absolute path instead.`);
|
|
3309
|
+
process.exit(1);
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
targetDir = realTarget;
|
|
3313
|
+
// Initialize AIM Core if requested
|
|
3314
|
+
let aimCore;
|
|
3315
|
+
if (options.withAim) {
|
|
3316
|
+
aimCore = new aim_core_1.AIMCore({
|
|
3317
|
+
agentName: path.basename(targetDir),
|
|
3318
|
+
dataDir: path.join(targetDir, '.opena2a', 'aim'),
|
|
3319
|
+
});
|
|
3320
|
+
}
|
|
3321
|
+
// Create and initialize plugins in execution order
|
|
3322
|
+
// Order matters: CredVault replaces creds, SignCrypt signs skills,
|
|
3323
|
+
// SkillGuard pins last so hashes reflect the final file state.
|
|
3324
|
+
const pluginFactories = [
|
|
3325
|
+
{ name: 'CredVault', create: credvault_1.createPlugin },
|
|
3326
|
+
{ name: 'SignCrypt', create: signcrypt_1.createPlugin },
|
|
3327
|
+
{ name: 'SkillGuard', create: skillguard_1.createPlugin },
|
|
3328
|
+
];
|
|
3329
|
+
const plugins = [];
|
|
3330
|
+
for (const factory of pluginFactories) {
|
|
3331
|
+
const plugin = factory.create();
|
|
3332
|
+
await plugin.init(aimCore ? { aimCore } : undefined);
|
|
3333
|
+
plugins.push({ name: factory.name, plugin });
|
|
3334
|
+
}
|
|
3335
|
+
if (!options.json) {
|
|
3336
|
+
console.log(`\n OpenA2A Fix-All Security Report`);
|
|
3337
|
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
|
3338
|
+
if (options.dryRun) {
|
|
3339
|
+
console.log(`Scanning ${targetDir} (dry-run -- previewing fixes)...\n`);
|
|
3340
|
+
}
|
|
3341
|
+
else if (options.scanOnly) {
|
|
3342
|
+
console.log(`Scanning ${targetDir} (scan-only -- no fixes applied)...\n`);
|
|
3343
|
+
}
|
|
3344
|
+
else {
|
|
3345
|
+
console.log(`Scanning and fixing ${targetDir}...\n`);
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
const results = [];
|
|
3349
|
+
let allFindings = [];
|
|
3350
|
+
let allRemediations = [];
|
|
3351
|
+
let pluginErrors = 0;
|
|
3352
|
+
for (const { name, plugin } of plugins) {
|
|
3353
|
+
if (!options.json) {
|
|
3354
|
+
console.log(`${colors.cyan}> ${name}${RESET()}`);
|
|
3355
|
+
}
|
|
3356
|
+
try {
|
|
3357
|
+
// Scan
|
|
3358
|
+
const findings = await plugin.scan(targetDir);
|
|
3359
|
+
let remediations = [];
|
|
3360
|
+
if (!options.scanOnly && findings.length > 0) {
|
|
3361
|
+
remediations = await plugin.fix(targetDir, {
|
|
3362
|
+
dryRun: options.dryRun ?? false,
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
results.push({ name, findings, remediations });
|
|
3366
|
+
allFindings.push(...findings);
|
|
3367
|
+
allRemediations.push(...remediations);
|
|
3368
|
+
if (!options.json) {
|
|
3369
|
+
if (findings.length === 0) {
|
|
3370
|
+
console.log(` ${colors.green}[+] No issues found${RESET()}`);
|
|
3371
|
+
}
|
|
3372
|
+
else {
|
|
3373
|
+
console.log(` Found ${findings.length} issue(s)`);
|
|
3374
|
+
if (remediations.length > 0) {
|
|
3375
|
+
console.log(` ${colors.green}[+] Fixed ${remediations.length}${RESET()}`);
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
console.log();
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
catch (pluginErr) {
|
|
3382
|
+
// Isolate plugin errors — one failing plugin should not crash the entire run
|
|
3383
|
+
pluginErrors++;
|
|
3384
|
+
results.push({ name, findings: [], remediations: [] });
|
|
3385
|
+
if (!options.json) {
|
|
3386
|
+
console.log(` ${colors.brightRed}[!!] Plugin error: ${pluginErr instanceof Error ? pluginErr.message : String(pluginErr)}${RESET()}`);
|
|
3387
|
+
if (pluginErr instanceof Error && pluginErr.stack) {
|
|
3388
|
+
console.error(pluginErr.stack);
|
|
3389
|
+
}
|
|
3390
|
+
console.log();
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
// JSON output
|
|
3395
|
+
if (options.json) {
|
|
3396
|
+
const unfixed = allFindings.filter((f) => !allRemediations.some((r) => r.findingId === f.id));
|
|
3397
|
+
const jsonOutput = {
|
|
3398
|
+
target: targetDir,
|
|
3399
|
+
mode: options.dryRun ? 'dry-run' : options.scanOnly ? 'scan-only' : 'fix',
|
|
3400
|
+
aimEnabled: !!aimCore,
|
|
3401
|
+
totalFindings: allFindings.length,
|
|
3402
|
+
totalFixed: allRemediations.length,
|
|
3403
|
+
remainingIssues: unfixed.length,
|
|
3404
|
+
pluginErrors,
|
|
3405
|
+
scanComplete: pluginErrors === 0,
|
|
3406
|
+
plugins: results.map((r) => ({
|
|
3407
|
+
name: r.name,
|
|
3408
|
+
findings: r.findings,
|
|
3409
|
+
remediations: r.remediations,
|
|
3410
|
+
})),
|
|
3411
|
+
};
|
|
3412
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
3413
|
+
if (pluginErrors > 0)
|
|
3414
|
+
process.exit(2);
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
// Summary
|
|
3418
|
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
3419
|
+
console.log(`\nFindings: ${allFindings.length} total | ${allRemediations.length} fixed\n`);
|
|
3420
|
+
// Show remaining issues (not auto-fixed)
|
|
3421
|
+
const fixedIds = new Set(allRemediations.map((r) => r.findingId));
|
|
3422
|
+
const remainingFindings = allFindings.filter((f) => !fixedIds.has(f.id) || !f.autoFixable);
|
|
3423
|
+
if (remainingFindings.length > 0) {
|
|
3424
|
+
console.log(`${colors.red}Remaining Issues (require manual review):${RESET()}\n`);
|
|
3425
|
+
for (const finding of remainingFindings) {
|
|
3426
|
+
const display = PLUGIN_SEVERITY_DISPLAY[finding.severity];
|
|
3427
|
+
console.log(`${display.color()}${display.symbol} [${finding.id}] ${finding.severity.toUpperCase()}${RESET()}`);
|
|
3428
|
+
console.log(` ${finding.title}`);
|
|
3429
|
+
console.log(` ${finding.description}`);
|
|
3430
|
+
if (finding.filePath) {
|
|
3431
|
+
console.log(` File: ${finding.filePath}`);
|
|
3432
|
+
}
|
|
3433
|
+
console.log();
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
// Show remediations applied
|
|
3437
|
+
if (allRemediations.length > 0 && !options.scanOnly) {
|
|
3438
|
+
const label = options.dryRun ? 'Fixes Available (dry-run):' : 'Fixes Applied:';
|
|
3439
|
+
console.log(`${colors.green}[+] ${label}${RESET()}\n`);
|
|
3440
|
+
for (const remediation of allRemediations) {
|
|
3441
|
+
console.log(` ${colors.green}[+]${RESET()} [${remediation.findingId}] ${remediation.description}`);
|
|
3442
|
+
if (remediation.filesModified.length > 0 && options.verbose) {
|
|
3443
|
+
for (const file of remediation.filesModified) {
|
|
3444
|
+
console.log(` ${colors.cyan}→${RESET()} ${file}`);
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
console.log();
|
|
3449
|
+
if (!options.dryRun) {
|
|
3450
|
+
console.log(`${colors.cyan}Note:${RESET()} Plugin data stored in ${targetDir}/.opena2a/`);
|
|
3451
|
+
console.log(` Uninstall with: hackmyagent fix-all ${directory || '.'} --uninstall\n`);
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
// All clear message
|
|
3455
|
+
if (allFindings.length === 0) {
|
|
3456
|
+
console.log(`${colors.green}[+] No security issues found. Agent looks good.${RESET()}\n`);
|
|
3457
|
+
}
|
|
3458
|
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
3459
|
+
console.log(`Run 'hackmyagent secure' for a full hardening scan.\n`);
|
|
3460
|
+
// Warn if scan is incomplete due to plugin errors
|
|
3461
|
+
if (pluginErrors > 0) {
|
|
3462
|
+
console.log(`\n${colors.brightRed}[!!] WARNING: ${pluginErrors} plugin(s) failed — scan results are incomplete${RESET()}`);
|
|
3463
|
+
console.log(` Re-run with --verbose for details.\n`);
|
|
3464
|
+
}
|
|
3465
|
+
// Exit with non-zero if critical/high issues remain or scan is incomplete
|
|
3466
|
+
if (pluginErrors > 0) {
|
|
3467
|
+
process.exit(2); // Exit 2 = partial/incomplete scan
|
|
3468
|
+
}
|
|
3469
|
+
const criticalOrHigh = remainingFindings.filter((f) => f.severity === 'critical' || f.severity === 'high');
|
|
3470
|
+
if (criticalOrHigh.length > 0) {
|
|
3471
|
+
process.exit(1);
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
catch (error) {
|
|
3475
|
+
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
3476
|
+
process.exit(1);
|
|
3477
|
+
}
|
|
3478
|
+
});
|
|
3479
|
+
// MCP Server command
|
|
3480
|
+
program
|
|
3481
|
+
.command('mcp-serve')
|
|
3482
|
+
.description('Run HackMyAgent as an MCP server (stdio transport)')
|
|
3483
|
+
.action(async () => {
|
|
3484
|
+
try {
|
|
3485
|
+
const { startMcpServer } = await Promise.resolve().then(() => __importStar(require('./mcp-server')));
|
|
3486
|
+
await startMcpServer();
|
|
3487
|
+
}
|
|
3488
|
+
catch (error) {
|
|
3489
|
+
console.error(`Error starting MCP server: ${error instanceof Error ? error.message : error}`);
|
|
3490
|
+
process.exit(1);
|
|
3491
|
+
}
|
|
3492
|
+
});
|
|
3493
|
+
// Init MCP command
|
|
3494
|
+
program
|
|
3495
|
+
.command('init-mcp')
|
|
3496
|
+
.description(`Add HackMyAgent as an MCP server to your AI coding tool
|
|
3497
|
+
|
|
3498
|
+
Detects your IDE (Claude Code, Cursor, VS Code) and configures
|
|
3499
|
+
HackMyAgent as an MCP server for LLM-powered security analysis.
|
|
3500
|
+
|
|
3501
|
+
Once configured, ask your AI assistant:
|
|
3502
|
+
"Run a deep security scan on this project"
|
|
3503
|
+
|
|
3504
|
+
Examples:
|
|
3505
|
+
$ hackmyagent init-mcp
|
|
3506
|
+
$ hackmyagent init-mcp --tool cursor
|
|
3507
|
+
$ hackmyagent init-mcp /path/to/project`)
|
|
3508
|
+
.argument('[directory]', 'Project directory (defaults to current directory)', '.')
|
|
3509
|
+
.option('-t, --tool <name>', 'Force specific tool: claude, cursor, vscode')
|
|
3510
|
+
.action(async (directory, options) => {
|
|
3511
|
+
try {
|
|
3512
|
+
const targetDir = directory.startsWith('/') ? directory : process.cwd() + '/' + directory;
|
|
3513
|
+
const { initMcp } = await Promise.resolve().then(() => __importStar(require('./init-mcp')));
|
|
3514
|
+
const result = initMcp(targetDir, options.tool);
|
|
3515
|
+
if (!result.created) {
|
|
3516
|
+
console.log(`\n HackMyAgent MCP server already configured in ${result.configPath}\n`);
|
|
3517
|
+
return;
|
|
3518
|
+
}
|
|
3519
|
+
console.log(`\n Detected: ${result.tool}\n`);
|
|
3520
|
+
console.log(` Added HackMyAgent MCP server to ${result.configPath}\n`);
|
|
3521
|
+
console.log(` Available tools in ${result.tool}:`);
|
|
3522
|
+
console.log(` hackmyagent_scan — 147+ checks + structural analysis`);
|
|
3523
|
+
console.log(` hackmyagent_deep_scan — Full analysis with LLM reasoning`);
|
|
3524
|
+
console.log(` hackmyagent_analyze_file — Analyze a single file`);
|
|
3525
|
+
console.log(` hackmyagent_benchmark — OASB-1 compliance assessment\n`);
|
|
3526
|
+
console.log(` Try: "Run a deep security scan on this project"\n`);
|
|
3527
|
+
}
|
|
3528
|
+
catch (error) {
|
|
3529
|
+
console.error(`Error: ${error instanceof Error ? error.message : error}`);
|
|
3530
|
+
process.exit(1);
|
|
3531
|
+
}
|
|
3532
|
+
});
|
|
3533
|
+
program.parse();
|
|
3534
|
+
//# sourceMappingURL=cli.js.map
|