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.
@@ -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 AIUC-1 compliance controls with evaluation criteria. Filter by domain (security/safety), control IDs, or OWASP LLM tags. Returns structured evaluation rules for pass/partial/fail assessment.",
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)');
@@ -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.1.0",
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 — AIUC-1 controls registry loader + schema validator.
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
- let _cache = null;
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 && !KNOWN_DOMAINS.has(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 the AIUC-1 controls registry. Validates on first 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', 'aiuc-1-controls.json');
119
- const data = JSON.parse(readFileSync(controlsPath, 'utf-8'));
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(`AIUC-1 controls registry validation failed:\n${errors.join('\n')}`);
184
+ throw new Error(`${framework} controls registry validation failed:\n${errors.join('\n')}`);
124
185
  }
125
186
 
126
- _cache = data;
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.domain] - 'security', 'safety', or 'all'
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 = null;
242
+ _cache.clear();
164
243
  }