agent-security-scanner-mcp 4.1.0 → 4.2.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/README.md +394 -1
- package/compliance/gdpr-technical-controls.json +112 -0
- package/compliance/soc2-technical-controls.json +148 -0
- package/index.js +148 -1
- package/openclaw.plugin.json +21 -1
- package/package.json +1 -1
- package/src/lib/compliance-controls.js +100 -21
- package/src/lib/compliance-evaluator.js +150 -9
- package/src/lib/compliance-evidence.js +321 -0
- package/src/lib/cyclonedx.js +113 -0
- package/src/lib/lockfile-parsers.js +671 -0
- package/src/lib/osv-client.js +254 -0
- package/src/lib/purl.js +90 -0
- package/src/lib/sbom-component.js +88 -0
- package/src/tools/compliance-controls.js +22 -12
- package/src/tools/evaluate-compliance.js +161 -0
- package/src/tools/sbom-diff.js +199 -0
- package/src/tools/sbom-generate.js +116 -0
- package/src/tools/sbom-hallucinations.js +117 -0
- package/src/tools/sbom-report.js +271 -0
- package/src/tools/sbom-vulnerabilities.js +121 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.1",
|
|
3
|
+
"framework": "SOC2-Technical",
|
|
4
|
+
"source": "AICPA Trust Services Criteria (2017) — technical controls subset",
|
|
5
|
+
"source_snapshot": "2026-03-31",
|
|
6
|
+
"source_note": "Technical controls only. This does not cover organizational, administrative, or physical SOC 2 controls. Not a substitute for a SOC 2 audit.",
|
|
7
|
+
"domains": ["security", "availability", "confidentiality", "supply-chain", "auth"],
|
|
8
|
+
"controls": [
|
|
9
|
+
{
|
|
10
|
+
"id": "SOC2-T001",
|
|
11
|
+
"title": "Software dependency inventory exists",
|
|
12
|
+
"domain": "supply-chain",
|
|
13
|
+
"references": ["CC6.6", "CC7.1"],
|
|
14
|
+
"scanner_tools": ["sbom_generate"],
|
|
15
|
+
"evidence_requirements": [
|
|
16
|
+
"CycloneDX SBOM generated from project lockfiles",
|
|
17
|
+
"Component count and ecosystem breakdown"
|
|
18
|
+
],
|
|
19
|
+
"evaluation": {
|
|
20
|
+
"evidence_checks": [
|
|
21
|
+
{ "path": "sbom.component_count", "operator": "gte", "value": 1, "on_fail": "fail", "reason": "No dependency inventory — SBOM generation found zero components" }
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "SOC2-T002",
|
|
27
|
+
"title": "No critical dependency vulnerabilities",
|
|
28
|
+
"domain": "supply-chain",
|
|
29
|
+
"references": ["CC6.6", "CC7.1", "CC7.2"],
|
|
30
|
+
"scanner_tools": ["sbom_generate", "sbom_scan_vulnerabilities"],
|
|
31
|
+
"evidence_requirements": [
|
|
32
|
+
"OSV vulnerability scan results for all SBOM components",
|
|
33
|
+
"Zero critical-severity known vulnerabilities"
|
|
34
|
+
],
|
|
35
|
+
"evaluation": {
|
|
36
|
+
"evidence_checks": [
|
|
37
|
+
{ "path": "supply_chain.vulnerabilities.by_severity.critical", "operator": "lte", "value": 0, "on_fail": "fail", "reason": "Critical dependency vulnerabilities found", "default": 0 },
|
|
38
|
+
{ "path": "supply_chain.vulnerabilities.by_severity.high", "operator": "lte", "value": 5, "on_fail": "partial", "reason": "High-severity dependency vulnerabilities exceed threshold", "default": 0 }
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "SOC2-T003",
|
|
44
|
+
"title": "No hallucinated (phantom) packages in dependency tree",
|
|
45
|
+
"domain": "supply-chain",
|
|
46
|
+
"references": ["CC6.6", "CC6.8"],
|
|
47
|
+
"scanner_tools": ["sbom_generate", "sbom_check_hallucinations"],
|
|
48
|
+
"evidence_requirements": [
|
|
49
|
+
"Hallucination check results for all SBOM components",
|
|
50
|
+
"Zero hallucinated packages detected"
|
|
51
|
+
],
|
|
52
|
+
"evaluation": {
|
|
53
|
+
"evidence_checks": [
|
|
54
|
+
{ "path": "supply_chain.hallucinations.hallucinated_count", "operator": "eq", "value": 0, "on_fail": "fail", "reason": "Hallucinated (phantom) packages detected in dependency tree" },
|
|
55
|
+
{ "path": "supply_chain.hallucinations.legitimate_count", "operator": "gte", "value": 1, "on_fail": "not_evaluated", "not_evaluated_reason": "No packages could be verified — all ecosystems unsupported by hallucination checker", "default": 0 }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "SOC2-T004",
|
|
61
|
+
"title": "No critical code security findings",
|
|
62
|
+
"domain": "security",
|
|
63
|
+
"references": ["CC6.1", "CC6.6", "CC7.2"],
|
|
64
|
+
"scanner_tools": ["scan_security", "scan_project"],
|
|
65
|
+
"evidence_requirements": [
|
|
66
|
+
"Static analysis scan with zero CRITICAL findings",
|
|
67
|
+
"Project-level security grade"
|
|
68
|
+
],
|
|
69
|
+
"evaluation": {
|
|
70
|
+
"evidence_checks": [
|
|
71
|
+
{ "path": "scan.by_category_severity.injection.CRITICAL", "operator": "lte", "value": 0, "on_fail": "fail", "reason": "Critical injection vulnerabilities found", "default": 0 },
|
|
72
|
+
{ "path": "scan.by_category_severity.deserialization.CRITICAL", "operator": "lte", "value": 0, "on_fail": "fail", "reason": "Critical deserialization vulnerabilities found", "default": 0 },
|
|
73
|
+
{ "path": "scan.by_severity.CRITICAL", "operator": "lte", "value": 0, "on_fail": "fail", "reason": "Critical code findings present", "default": 0 }
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "SOC2-T005",
|
|
79
|
+
"title": "Data exfiltration and information exposure below threshold",
|
|
80
|
+
"domain": "confidentiality",
|
|
81
|
+
"references": ["CC6.1", "CC6.5", "C1.1"],
|
|
82
|
+
"scanner_tools": ["scan_security", "scan_project"],
|
|
83
|
+
"evidence_requirements": [
|
|
84
|
+
"Exfiltration pattern scan results",
|
|
85
|
+
"Information exposure findings below threshold"
|
|
86
|
+
],
|
|
87
|
+
"evaluation": {
|
|
88
|
+
"evidence_checks": [
|
|
89
|
+
{ "path": "scan.by_category_severity.exfiltration.CRITICAL", "operator": "lte", "value": 0, "on_fail": "fail", "reason": "Critical exfiltration findings detected", "default": 0 },
|
|
90
|
+
{ "path": "scan.by_category_severity.exfiltration.HIGH", "operator": "lte", "value": 0, "on_fail": "fail", "reason": "High-severity exfiltration findings detected", "default": 0 },
|
|
91
|
+
{ "path": "scan.by_category_severity.info-exposure.CRITICAL", "operator": "lte", "value": 0, "on_fail": "fail", "reason": "Critical information exposure findings detected", "default": 0 },
|
|
92
|
+
{ "path": "scan.by_category_severity.info-exposure.HIGH", "operator": "lte", "value": 3, "on_fail": "partial", "reason": "High-severity information exposure findings exceed threshold", "default": 0 }
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"id": "SOC2-T006",
|
|
98
|
+
"title": "Cryptographic controls adequate",
|
|
99
|
+
"domain": "confidentiality",
|
|
100
|
+
"references": ["CC6.1", "CC6.7", "C1.1"],
|
|
101
|
+
"scanner_tools": ["scan_security", "scan_project"],
|
|
102
|
+
"evidence_requirements": [
|
|
103
|
+
"No critical/high crypto findings (weak algorithms, hardcoded keys)",
|
|
104
|
+
"Encryption usage scan results"
|
|
105
|
+
],
|
|
106
|
+
"evaluation": {
|
|
107
|
+
"evidence_checks": [
|
|
108
|
+
{ "path": "scan.by_category_severity.crypto.CRITICAL", "operator": "lte", "value": 0, "on_fail": "fail", "reason": "Critical cryptographic findings (weak algorithms, hardcoded keys)", "default": 0 },
|
|
109
|
+
{ "path": "scan.by_category_severity.crypto.HIGH", "operator": "lte", "value": 2, "on_fail": "partial", "reason": "High-severity cryptographic findings exceed threshold", "default": 0 }
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"id": "SOC2-T007",
|
|
115
|
+
"title": "Authentication and authorization controls adequate",
|
|
116
|
+
"domain": "auth",
|
|
117
|
+
"references": ["CC6.1", "CC6.2", "CC6.3"],
|
|
118
|
+
"scanner_tools": ["scan_security", "scan_project"],
|
|
119
|
+
"evidence_requirements": [
|
|
120
|
+
"No critical auth findings (hardcoded creds, missing auth checks)",
|
|
121
|
+
"Least-privilege and permissions scan results"
|
|
122
|
+
],
|
|
123
|
+
"evaluation": {
|
|
124
|
+
"evidence_checks": [
|
|
125
|
+
{ "path": "scan.by_category_severity.auth.CRITICAL", "operator": "lte", "value": 0, "on_fail": "fail", "reason": "Critical authentication/authorization findings detected", "default": 0 },
|
|
126
|
+
{ "path": "scan.by_category_severity.auth.HIGH", "operator": "lte", "value": 2, "on_fail": "partial", "reason": "High-severity auth findings exceed threshold", "default": 0 },
|
|
127
|
+
{ "path": "scan.by_category_severity.permissions.CRITICAL", "operator": "lte", "value": 0, "on_fail": "fail", "reason": "Critical permissions/least-privilege findings detected", "default": 0 }
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"id": "SOC2-T008",
|
|
133
|
+
"title": "Dependency drift tracked when baseline exists",
|
|
134
|
+
"domain": "supply-chain",
|
|
135
|
+
"references": ["CC6.6", "CC8.1"],
|
|
136
|
+
"scanner_tools": ["sbom_generate", "sbom_diff"],
|
|
137
|
+
"evidence_requirements": [
|
|
138
|
+
"SBOM baseline comparison results",
|
|
139
|
+
"Change tracking for added, removed, and version-changed packages"
|
|
140
|
+
],
|
|
141
|
+
"evaluation": {
|
|
142
|
+
"evidence_checks": [
|
|
143
|
+
{ "path": "supply_chain.drift.baseline_exists", "operator": "eq", "value": true, "on_fail": "not_evaluated", "not_evaluated_reason": "No SBOM baseline — dependency drift cannot be evaluated" }
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
}
|
package/index.js
CHANGED
|
@@ -28,6 +28,12 @@ import { runInitHooks } from './src/cli/init-hooks.js';
|
|
|
28
28
|
import { runReport } from './src/cli/report.js';
|
|
29
29
|
import { scoreAivssSchema, scoreAivssTool } from './src/tools/score-aivss.js';
|
|
30
30
|
import { complianceControlsSchema, getComplianceControls } from './src/tools/compliance-controls.js';
|
|
31
|
+
import { sbomGenerateSchema, sbomGenerate } from './src/tools/sbom-generate.js';
|
|
32
|
+
import { sbomVulnerabilitiesSchema, sbomScanVulnerabilities } from './src/tools/sbom-vulnerabilities.js';
|
|
33
|
+
import { sbomHallucinationsSchema, sbomCheckHallucinations } from './src/tools/sbom-hallucinations.js';
|
|
34
|
+
import { sbomDiffSchema, sbomDiff } from './src/tools/sbom-diff.js';
|
|
35
|
+
import { sbomReportSchema, sbomExportReport } from './src/tools/sbom-report.js';
|
|
36
|
+
import { evaluateComplianceSchema, evaluateCompliance } from './src/tools/evaluate-compliance.js';
|
|
31
37
|
|
|
32
38
|
// Handle both ESM and CJS bundling (Smithery bundles to CJS)
|
|
33
39
|
let __dirname;
|
|
@@ -247,11 +253,57 @@ server.tool(
|
|
|
247
253
|
|
|
248
254
|
server.tool(
|
|
249
255
|
"get_compliance_controls",
|
|
250
|
-
"Look up
|
|
256
|
+
"Look up compliance controls with evaluation criteria. Supports multiple frameworks: aiuc-1 (default), soc2-technical, gdpr-technical. Filter by domain, control IDs, or OWASP LLM tags.",
|
|
251
257
|
complianceControlsSchema,
|
|
252
258
|
getComplianceControls
|
|
253
259
|
);
|
|
254
260
|
|
|
261
|
+
server.tool(
|
|
262
|
+
"evaluate_compliance",
|
|
263
|
+
"Evaluate a project against compliance frameworks (SOC2-technical, GDPR-technical, AIUC-1). Collects evidence from code scans, SBOM, vulnerability checks, and hallucination detection, then evaluates controls. Optionally saves timestamped evidence bundle.",
|
|
264
|
+
evaluateComplianceSchema,
|
|
265
|
+
evaluateCompliance
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// ===========================================
|
|
269
|
+
// SBOM / SUPPLY CHAIN ANALYSIS
|
|
270
|
+
// ===========================================
|
|
271
|
+
|
|
272
|
+
server.tool(
|
|
273
|
+
"sbom_generate",
|
|
274
|
+
"Generate a CycloneDX v1.5 SBOM for a project. Discovers all dependencies (direct + transitive) from lock files and manifests across Node.js, Python, Go, Rust, Ruby, Java. Use verbosity='minimal' for counts, 'compact' (default) for component list, 'full' for complete CycloneDX JSON.",
|
|
275
|
+
sbomGenerateSchema,
|
|
276
|
+
sbomGenerate
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
server.tool(
|
|
280
|
+
"sbom_scan_vulnerabilities",
|
|
281
|
+
"Cross-reference SBOM components against OSV.dev vulnerability database. Returns CVE IDs, CVSS scores, severity, and fix recommendations. Accepts directory_path (generates fresh) or sbom_path (loads saved artifact).",
|
|
282
|
+
sbomVulnerabilitiesSchema,
|
|
283
|
+
sbomScanVulnerabilities
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
server.tool(
|
|
287
|
+
"sbom_check_hallucinations",
|
|
288
|
+
"Check all packages in an SBOM against official registries to detect hallucinated (AI-invented) package names. Supports npm, pypi, rubygems, dart, perl, raku, crates. Go/Java marked as unsupported.",
|
|
289
|
+
sbomHallucinationsSchema,
|
|
290
|
+
sbomCheckHallucinations
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
server.tool(
|
|
294
|
+
"sbom_diff",
|
|
295
|
+
"Compare current project SBOM against a stored baseline. Reports added, removed, and version-changed packages. Use save_baseline=true to create initial baseline.",
|
|
296
|
+
sbomDiffSchema,
|
|
297
|
+
sbomDiff
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
server.tool(
|
|
301
|
+
"sbom_export_report",
|
|
302
|
+
"Generate an HTML or JSON audit report from SBOM data, optionally enriched with vulnerability scan results. Suitable for PCI-DSS and compliance audits.",
|
|
303
|
+
sbomReportSchema,
|
|
304
|
+
sbomExportReport
|
|
305
|
+
);
|
|
306
|
+
|
|
255
307
|
// ===========================================
|
|
256
308
|
// CLI COMMANDS - Extracted to src/cli/
|
|
257
309
|
// ===========================================
|
|
@@ -524,6 +576,95 @@ const cliArgs = process.argv.slice(2);
|
|
|
524
576
|
console.error(` Error: ${err.message}\n`);
|
|
525
577
|
process.exit(1);
|
|
526
578
|
});
|
|
579
|
+
} else if (cliArgs[0] === 'sbom-generate') {
|
|
580
|
+
const dirPath = cliArgs[1] || '.';
|
|
581
|
+
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
582
|
+
const verbosity = verbosityIdx !== -1 ? cliArgs[verbosityIdx + 1] : 'compact';
|
|
583
|
+
const save = cliArgs.includes('--save');
|
|
584
|
+
const outIdx = cliArgs.indexOf('--output');
|
|
585
|
+
const outputPath = outIdx !== -1 ? cliArgs[outIdx + 1] : (save ? join(dirPath, '.scanner', 'sbom.json') : undefined);
|
|
586
|
+
|
|
587
|
+
sbomGenerate({ directory_path: dirPath, output_path: outputPath, verbosity }).then(result => {
|
|
588
|
+
const output = JSON.parse(result.content[0].text);
|
|
589
|
+
console.log(JSON.stringify(output, null, 2));
|
|
590
|
+
process.exit(0);
|
|
591
|
+
}).catch(err => {
|
|
592
|
+
console.error(JSON.stringify({ error: err.message }));
|
|
593
|
+
process.exit(1);
|
|
594
|
+
});
|
|
595
|
+
} else if (cliArgs[0] === 'sbom-vulnerabilities') {
|
|
596
|
+
const dirPath = cliArgs[1] || '.';
|
|
597
|
+
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
598
|
+
const verbosity = verbosityIdx !== -1 ? cliArgs[verbosityIdx + 1] : 'compact';
|
|
599
|
+
const sbomIdx = cliArgs.indexOf('--sbom-path');
|
|
600
|
+
const sbomPath = sbomIdx !== -1 ? cliArgs[sbomIdx + 1] : undefined;
|
|
601
|
+
|
|
602
|
+
sbomScanVulnerabilities({
|
|
603
|
+
directory_path: sbomPath ? undefined : dirPath,
|
|
604
|
+
sbom_path: sbomPath,
|
|
605
|
+
verbosity,
|
|
606
|
+
}).then(result => {
|
|
607
|
+
const output = JSON.parse(result.content[0].text);
|
|
608
|
+
console.log(JSON.stringify(output, null, 2));
|
|
609
|
+
process.exit(output.total_vulnerabilities > 0 ? 1 : 0);
|
|
610
|
+
}).catch(err => {
|
|
611
|
+
console.error(JSON.stringify({ error: err.message }));
|
|
612
|
+
process.exit(1);
|
|
613
|
+
});
|
|
614
|
+
} else if (cliArgs[0] === 'sbom-check-hallucinations') {
|
|
615
|
+
const dirPath = cliArgs[1] || '.';
|
|
616
|
+
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
617
|
+
const verbosity = verbosityIdx !== -1 ? cliArgs[verbosityIdx + 1] : 'compact';
|
|
618
|
+
|
|
619
|
+
loadPackageLists();
|
|
620
|
+
sbomCheckHallucinations({ directory_path: dirPath, verbosity }).then(result => {
|
|
621
|
+
const output = JSON.parse(result.content[0].text);
|
|
622
|
+
console.log(JSON.stringify(output, null, 2));
|
|
623
|
+
process.exit(output.hallucinated_count > 0 ? 1 : 0);
|
|
624
|
+
}).catch(err => {
|
|
625
|
+
console.error(JSON.stringify({ error: err.message }));
|
|
626
|
+
process.exit(1);
|
|
627
|
+
});
|
|
628
|
+
} else if (cliArgs[0] === 'sbom-diff') {
|
|
629
|
+
const dirPath = cliArgs[1] || '.';
|
|
630
|
+
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
631
|
+
const verbosity = verbosityIdx !== -1 ? cliArgs[verbosityIdx + 1] : 'compact';
|
|
632
|
+
const saveBaseline = cliArgs.includes('--save-baseline');
|
|
633
|
+
const baselineIdx = cliArgs.indexOf('--baseline-path');
|
|
634
|
+
const baselinePath = baselineIdx !== -1 ? cliArgs[baselineIdx + 1] : undefined;
|
|
635
|
+
|
|
636
|
+
sbomDiff({ directory_path: dirPath, baseline_path: baselinePath, save_baseline: saveBaseline, verbosity }).then(result => {
|
|
637
|
+
const output = JSON.parse(result.content[0].text);
|
|
638
|
+
console.log(JSON.stringify(output, null, 2));
|
|
639
|
+
process.exit(0);
|
|
640
|
+
}).catch(err => {
|
|
641
|
+
console.error(JSON.stringify({ error: err.message }));
|
|
642
|
+
process.exit(1);
|
|
643
|
+
});
|
|
644
|
+
} else if (cliArgs[0] === 'sbom-report') {
|
|
645
|
+
const dirPath = cliArgs[1] || '.';
|
|
646
|
+
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
647
|
+
const verbosity = verbosityIdx !== -1 ? cliArgs[verbosityIdx + 1] : 'compact';
|
|
648
|
+
const formatIdx = cliArgs.indexOf('--format');
|
|
649
|
+
const format = formatIdx !== -1 ? cliArgs[formatIdx + 1] : 'html';
|
|
650
|
+
const outIdx = cliArgs.indexOf('--output');
|
|
651
|
+
const outputPath = outIdx !== -1 ? cliArgs[outIdx + 1] : join(dirPath, '.scanner', 'sbom-report.html');
|
|
652
|
+
const noVulns = cliArgs.includes('--no-vulnerabilities');
|
|
653
|
+
|
|
654
|
+
sbomExportReport({
|
|
655
|
+
directory_path: dirPath,
|
|
656
|
+
format,
|
|
657
|
+
include_vulnerabilities: !noVulns,
|
|
658
|
+
output_path: outputPath,
|
|
659
|
+
verbosity,
|
|
660
|
+
}).then(result => {
|
|
661
|
+
const output = JSON.parse(result.content[0].text);
|
|
662
|
+
console.log(JSON.stringify(output, null, 2));
|
|
663
|
+
process.exit(0);
|
|
664
|
+
}).catch(err => {
|
|
665
|
+
console.error(JSON.stringify({ error: err.message }));
|
|
666
|
+
process.exit(1);
|
|
667
|
+
});
|
|
527
668
|
} else if (cliArgs[0] === 'scan-clawhub') {
|
|
528
669
|
// Import and run SAFE ClawHub scanner (no code execution)
|
|
529
670
|
await import('./src/cli/scan-clawhub-safe.js');
|
|
@@ -550,6 +691,12 @@ const cliArgs = process.argv.slice(2);
|
|
|
550
691
|
console.log(' scan-action <t> <v> Check agent action before execution');
|
|
551
692
|
console.log(' audit [--config-path] Audit OpenClaw config for security issues [experimental]');
|
|
552
693
|
console.log(' harden [--fix] Auto-harden OpenClaw configuration [experimental]\n');
|
|
694
|
+
console.log(' SBOM / Supply Chain:');
|
|
695
|
+
console.log(' sbom-generate <dir> Generate CycloneDX SBOM [--save] [--output <path>]');
|
|
696
|
+
console.log(' sbom-vulnerabilities <dir> Scan SBOM against OSV.dev [--sbom-path <path>]');
|
|
697
|
+
console.log(' sbom-check-hallucinations <dir> Check SBOM packages against registries');
|
|
698
|
+
console.log(' sbom-diff <dir> Compare SBOM against baseline [--save-baseline]');
|
|
699
|
+
console.log(' sbom-report <dir> Generate SBOM audit report [--format html|json]\n');
|
|
553
700
|
console.log(' (no args) Start MCP server on stdio\n');
|
|
554
701
|
console.log(' Options:');
|
|
555
702
|
console.log(' --verbosity <level> minimal|compact|full (default: compact)');
|
package/openclaw.plugin.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"openclaw": {
|
|
8
8
|
"extensions": ["tools", "skills"],
|
|
9
9
|
"emoji": "shield",
|
|
10
|
-
"permissions": ["fs:read"]
|
|
10
|
+
"permissions": ["fs:read", "fs:write"]
|
|
11
11
|
},
|
|
12
12
|
"tools": [
|
|
13
13
|
{
|
|
@@ -33,6 +33,26 @@
|
|
|
33
33
|
{
|
|
34
34
|
"name": "scanner_health",
|
|
35
35
|
"description": "Check plugin health status"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "sbom_generate",
|
|
39
|
+
"description": "Generate CycloneDX SBOM for a project"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "sbom_scan_vulnerabilities",
|
|
43
|
+
"description": "Scan SBOM against OSV.dev vulnerability database"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "sbom_check_hallucinations",
|
|
47
|
+
"description": "Check SBOM packages against official registries"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "sbom_diff",
|
|
51
|
+
"description": "Compare current SBOM against stored baseline"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "sbom_export_report",
|
|
55
|
+
"description": "Generate HTML/JSON SBOM audit report"
|
|
36
56
|
}
|
|
37
57
|
],
|
|
38
58
|
"skills": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-security-scanner-mcp",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"mcpName": "io.github.sinewaveai/agent-security-scanner-mcp",
|
|
5
5
|
"description": "Security scanner MCP server for AI coding agents. Prompt injection firewall, package hallucination detection (4.3M+ packages), 1700+ vulnerability rules with AST & taint analysis, LLM-powered semantic code review, auto-fix. For Claude Code, Cursor, Windsurf, Cline, OpenClaw.",
|
|
6
6
|
"main": "index.js",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// src/lib/compliance-controls.js —
|
|
1
|
+
// src/lib/compliance-controls.js — Multi-framework controls registry loader + schema validator.
|
|
2
2
|
|
|
3
|
-
import { readFileSync } from 'fs';
|
|
3
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
4
4
|
import { join, dirname } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
|
|
@@ -11,14 +11,15 @@ try {
|
|
|
11
11
|
__dirname = process.cwd();
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const KNOWN_DOMAINS = new Set(['security', 'safety']);
|
|
15
14
|
const KNOWN_TOOLS = new Set([
|
|
16
15
|
'scan_security', 'scan_agent_prompt', 'scan_project', 'scan_skill',
|
|
17
16
|
'scan_mcp_server', 'scan_agent_action', 'scan_git_diff',
|
|
17
|
+
'sbom_generate', 'sbom_scan_vulnerabilities', 'sbom_check_hallucinations', 'sbom_diff',
|
|
18
18
|
]);
|
|
19
19
|
const OWASP_TAG_RE = /^LLM\d{2}$/;
|
|
20
|
+
const VALID_CHECK_OPS = new Set(['exists', 'eq', 'lte', 'gte']);
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
const _cache = new Map();
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Validate the controls registry schema. Returns array of error strings (empty = valid).
|
|
@@ -36,6 +37,13 @@ export function validateRegistry(data) {
|
|
|
36
37
|
return errors;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
// Validate registry-level domains array
|
|
41
|
+
if (!Array.isArray(data.domains) || data.domains.length === 0) {
|
|
42
|
+
errors.push('Registry must have a non-empty "domains" array');
|
|
43
|
+
return errors;
|
|
44
|
+
}
|
|
45
|
+
const registryDomains = new Set(data.domains);
|
|
46
|
+
|
|
39
47
|
const ids = new Set();
|
|
40
48
|
for (const ctrl of data.controls) {
|
|
41
49
|
// Required fields
|
|
@@ -50,9 +58,9 @@ export function validateRegistry(data) {
|
|
|
50
58
|
}
|
|
51
59
|
ids.add(ctrl.id);
|
|
52
60
|
|
|
53
|
-
// Domain validation
|
|
54
|
-
if (ctrl.domain && !
|
|
55
|
-
errors.push(`Control ${ctrl.id}: unknown domain "${ctrl.domain}"`);
|
|
61
|
+
// Domain validation — check against this registry's own domains
|
|
62
|
+
if (ctrl.domain && !registryDomains.has(ctrl.domain)) {
|
|
63
|
+
errors.push(`Control ${ctrl.id}: unknown domain "${ctrl.domain}" (registry allows: ${data.domains.join(', ')})`);
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
// Scanner tools validation
|
|
@@ -64,7 +72,7 @@ export function validateRegistry(data) {
|
|
|
64
72
|
}
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
// OWASP tags validation
|
|
75
|
+
// OWASP tags validation (optional — only validated when present)
|
|
68
76
|
if (Array.isArray(ctrl.owasp_llm)) {
|
|
69
77
|
for (const tag of ctrl.owasp_llm) {
|
|
70
78
|
if (!OWASP_TAG_RE.test(tag)) {
|
|
@@ -73,6 +81,19 @@ export function validateRegistry(data) {
|
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
83
|
|
|
84
|
+
// References validation (optional — array of strings)
|
|
85
|
+
if (ctrl.references !== undefined) {
|
|
86
|
+
if (!Array.isArray(ctrl.references)) {
|
|
87
|
+
errors.push(`Control ${ctrl.id}: references must be an array`);
|
|
88
|
+
} else {
|
|
89
|
+
for (const ref of ctrl.references) {
|
|
90
|
+
if (typeof ref !== 'string') {
|
|
91
|
+
errors.push(`Control ${ctrl.id}: each reference must be a string`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
76
97
|
// Evaluation field types
|
|
77
98
|
if (ctrl.evaluation) {
|
|
78
99
|
const ev = ctrl.evaluation;
|
|
@@ -102,6 +123,39 @@ export function validateRegistry(data) {
|
|
|
102
123
|
if (ev.min_grade !== undefined && typeof ev.min_grade !== 'string') {
|
|
103
124
|
errors.push(`Control ${ctrl.id}: evaluation.min_grade must be a string`);
|
|
104
125
|
}
|
|
126
|
+
|
|
127
|
+
// evidence_checks validation (generic path-based checks for SOC2/GDPR controls)
|
|
128
|
+
if (ev.evidence_checks !== undefined) {
|
|
129
|
+
if (!Array.isArray(ev.evidence_checks)) {
|
|
130
|
+
errors.push(`Control ${ctrl.id}: evaluation.evidence_checks must be an array`);
|
|
131
|
+
} else {
|
|
132
|
+
for (let i = 0; i < ev.evidence_checks.length; i++) {
|
|
133
|
+
const check = ev.evidence_checks[i];
|
|
134
|
+
const prefix = `Control ${ctrl.id}: evidence_checks[${i}]`;
|
|
135
|
+
if (!check.path || typeof check.path !== 'string') {
|
|
136
|
+
errors.push(`${prefix}: must have a string "path"`);
|
|
137
|
+
}
|
|
138
|
+
if (!check.operator || !VALID_CHECK_OPS.has(check.operator)) {
|
|
139
|
+
errors.push(`${prefix}: operator must be one of: ${[...VALID_CHECK_OPS].join(', ')}`);
|
|
140
|
+
}
|
|
141
|
+
if (check.operator !== 'exists' && check.value === undefined) {
|
|
142
|
+
errors.push(`${prefix}: non-exists operators require a "value"`);
|
|
143
|
+
}
|
|
144
|
+
if (!check.on_fail || !['fail', 'partial', 'not_evaluated'].includes(check.on_fail)) {
|
|
145
|
+
errors.push(`${prefix}: on_fail must be "fail", "partial", or "not_evaluated"`);
|
|
146
|
+
}
|
|
147
|
+
if (check.not_evaluated_reason !== undefined && typeof check.not_evaluated_reason !== 'string') {
|
|
148
|
+
errors.push(`${prefix}: not_evaluated_reason must be a string`);
|
|
149
|
+
}
|
|
150
|
+
if (check.reason !== undefined && typeof check.reason !== 'string') {
|
|
151
|
+
errors.push(`${prefix}: reason must be a string`);
|
|
152
|
+
}
|
|
153
|
+
if (check.default !== undefined && typeof check.default !== 'number' && typeof check.default !== 'boolean') {
|
|
154
|
+
errors.push(`${prefix}: default must be a number or boolean`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
105
159
|
}
|
|
106
160
|
}
|
|
107
161
|
|
|
@@ -109,34 +163,42 @@ export function validateRegistry(data) {
|
|
|
109
163
|
}
|
|
110
164
|
|
|
111
165
|
/**
|
|
112
|
-
* Load
|
|
166
|
+
* Load a controls registry by framework name. Validates on first load per framework.
|
|
167
|
+
* @param {string} [framework='aiuc-1'] - Framework identifier (maps to compliance/<framework>-controls.json)
|
|
113
168
|
* @returns {object} The full registry object
|
|
114
169
|
*/
|
|
115
|
-
export function loadControls() {
|
|
116
|
-
if (_cache) return _cache;
|
|
117
|
-
|
|
118
|
-
const controlsPath = join(__dirname, '..', '..', 'compliance',
|
|
119
|
-
|
|
170
|
+
export function loadControls(framework = 'aiuc-1') {
|
|
171
|
+
if (_cache.has(framework)) return _cache.get(framework);
|
|
172
|
+
|
|
173
|
+
const controlsPath = join(__dirname, '..', '..', 'compliance', `${framework}-controls.json`);
|
|
174
|
+
let raw;
|
|
175
|
+
try {
|
|
176
|
+
raw = readFileSync(controlsPath, 'utf-8');
|
|
177
|
+
} catch (err) {
|
|
178
|
+
throw new Error(`Cannot load controls registry for framework "${framework}": ${err.message}`);
|
|
179
|
+
}
|
|
180
|
+
const data = JSON.parse(raw);
|
|
120
181
|
|
|
121
182
|
const errors = validateRegistry(data);
|
|
122
183
|
if (errors.length > 0) {
|
|
123
|
-
throw new Error(
|
|
184
|
+
throw new Error(`${framework} controls registry validation failed:\n${errors.join('\n')}`);
|
|
124
185
|
}
|
|
125
186
|
|
|
126
|
-
_cache
|
|
187
|
+
_cache.set(framework, data);
|
|
127
188
|
return data;
|
|
128
189
|
}
|
|
129
190
|
|
|
130
191
|
/**
|
|
131
|
-
* Filter controls by domain, control IDs, or OWASP tags.
|
|
192
|
+
* Filter controls by framework, domain, control IDs, or OWASP tags.
|
|
132
193
|
* @param {object} [filters]
|
|
133
|
-
* @param {string} [filters.
|
|
194
|
+
* @param {string} [filters.framework] - Framework to load (default: 'aiuc-1')
|
|
195
|
+
* @param {string} [filters.domain] - Domain name or 'all'
|
|
134
196
|
* @param {string[]} [filters.controlIds] - Specific control IDs
|
|
135
197
|
* @param {string[]} [filters.owaspFilter] - OWASP LLM tags to match
|
|
136
198
|
* @returns {object[]} Filtered controls
|
|
137
199
|
*/
|
|
138
|
-
export function filterControls({ domain, controlIds, owaspFilter } = {}) {
|
|
139
|
-
const registry = loadControls();
|
|
200
|
+
export function filterControls({ framework = 'aiuc-1', domain, controlIds, owaspFilter } = {}) {
|
|
201
|
+
const registry = loadControls(framework);
|
|
140
202
|
let controls = registry.controls;
|
|
141
203
|
|
|
142
204
|
if (domain && domain !== 'all') {
|
|
@@ -158,7 +220,24 @@ export function filterControls({ domain, controlIds, owaspFilter } = {}) {
|
|
|
158
220
|
return controls;
|
|
159
221
|
}
|
|
160
222
|
|
|
223
|
+
/**
|
|
224
|
+
* List available framework names by scanning the compliance directory.
|
|
225
|
+
* @returns {string[]} Available framework identifiers
|
|
226
|
+
*/
|
|
227
|
+
export function listFrameworks() {
|
|
228
|
+
const dir = join(__dirname, '..', '..', 'compliance');
|
|
229
|
+
try {
|
|
230
|
+
return readdirSync(dir)
|
|
231
|
+
.filter(f => f.endsWith('-controls.json'))
|
|
232
|
+
.map(f => f.replace('-controls.json', ''));
|
|
233
|
+
} catch {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export { KNOWN_TOOLS };
|
|
239
|
+
|
|
161
240
|
// Reset cache (for testing)
|
|
162
241
|
export function _resetCache() {
|
|
163
|
-
_cache
|
|
242
|
+
_cache.clear();
|
|
164
243
|
}
|