agent-security-scanner-mcp 3.19.0 → 3.20.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/compliance/aiuc-1-controls.json +330 -0
- package/index.js +21 -1
- package/package.json +4 -2
- package/src/cli/report.js +71 -0
- package/src/lib/aivss.js +284 -0
- package/src/lib/compliance-controls.js +164 -0
- package/src/lib/compliance-evaluator.js +149 -0
- package/src/lib/normalize-finding.js +146 -0
- package/src/tools/compliance-controls.js +67 -0
- package/src/tools/score-aivss.js +98 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// src/lib/normalize-finding.js — Normalize findings from 6 different tool shapes into one internal format.
|
|
2
|
+
|
|
3
|
+
const SEVERITY_MAP = {
|
|
4
|
+
'error': { severity: 'HIGH', severity_rank: 3 },
|
|
5
|
+
'ERROR': { severity: 'HIGH', severity_rank: 3 },
|
|
6
|
+
'warning': { severity: 'MEDIUM', severity_rank: 2 },
|
|
7
|
+
'WARNING': { severity: 'MEDIUM', severity_rank: 2 },
|
|
8
|
+
'info': { severity: 'INFO', severity_rank: 0 },
|
|
9
|
+
'INFO': { severity: 'INFO', severity_rank: 0 },
|
|
10
|
+
'CRITICAL': { severity: 'CRITICAL', severity_rank: 4 },
|
|
11
|
+
'critical': { severity: 'CRITICAL', severity_rank: 4 },
|
|
12
|
+
'LOW': { severity: 'LOW', severity_rank: 1 },
|
|
13
|
+
'low': { severity: 'LOW', severity_rank: 1 },
|
|
14
|
+
'HIGH': { severity: 'HIGH', severity_rank: 3 },
|
|
15
|
+
'high': { severity: 'HIGH', severity_rank: 3 },
|
|
16
|
+
'MEDIUM': { severity: 'MEDIUM', severity_rank: 2 },
|
|
17
|
+
'medium': { severity: 'MEDIUM', severity_rank: 2 },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const DEFAULT_SEVERITY = { severity: 'MEDIUM', severity_rank: 2 };
|
|
21
|
+
|
|
22
|
+
// Map ruleId segments to categories for tools that don't emit category directly.
|
|
23
|
+
// Keyed by the second dotted segment of ruleId (e.g. "injection" from "python.injection.sql-injection").
|
|
24
|
+
const RULE_CATEGORY_MAP = {
|
|
25
|
+
'injection': 'injection',
|
|
26
|
+
'crypto': 'crypto',
|
|
27
|
+
'auth': 'auth',
|
|
28
|
+
'xss': 'xss',
|
|
29
|
+
'ssrf': 'ssrf',
|
|
30
|
+
'path': 'path-traversal',
|
|
31
|
+
'deserialization': 'deserialization',
|
|
32
|
+
'info': 'info-exposure',
|
|
33
|
+
'permissions': 'permissions',
|
|
34
|
+
'logging': 'info-exposure',
|
|
35
|
+
'secrets': 'info-exposure',
|
|
36
|
+
'prompt': 'prompt-injection',
|
|
37
|
+
'exfiltration': 'exfiltration',
|
|
38
|
+
'supply': 'supply-chain',
|
|
39
|
+
'command': 'injection',
|
|
40
|
+
'sql': 'injection',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract a unified rule_id from any finding shape.
|
|
45
|
+
*/
|
|
46
|
+
function extractRuleId(finding) {
|
|
47
|
+
return finding.ruleId || finding.rule_id || finding.id || finding.rule || null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Infer category from ruleId when no explicit category is set.
|
|
52
|
+
* Looks at dotted segments of the ruleId for known category keywords.
|
|
53
|
+
*/
|
|
54
|
+
function inferCategory(ruleId) {
|
|
55
|
+
if (!ruleId) return null;
|
|
56
|
+
const segments = ruleId.toLowerCase().split('.');
|
|
57
|
+
for (const seg of segments) {
|
|
58
|
+
if (RULE_CATEGORY_MAP[seg]) return RULE_CATEGORY_MAP[seg];
|
|
59
|
+
}
|
|
60
|
+
// Check if any segment contains a known keyword
|
|
61
|
+
for (const seg of segments) {
|
|
62
|
+
for (const [key, cat] of Object.entries(RULE_CATEGORY_MAP)) {
|
|
63
|
+
if (seg.includes(key)) return cat;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Normalize confidence to uppercase HIGH/MEDIUM/LOW.
|
|
71
|
+
*/
|
|
72
|
+
function normalizeConfidence(confidence) {
|
|
73
|
+
if (!confidence) return 'MEDIUM';
|
|
74
|
+
const upper = String(confidence).toUpperCase();
|
|
75
|
+
if (upper === 'HIGH' || upper === 'MEDIUM' || upper === 'LOW') return upper;
|
|
76
|
+
return 'MEDIUM';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Normalize action to uppercase BLOCK/WARN/ALLOW or null.
|
|
81
|
+
*/
|
|
82
|
+
function normalizeAction(action) {
|
|
83
|
+
if (!action) return null;
|
|
84
|
+
const upper = String(action).toUpperCase();
|
|
85
|
+
if (upper === 'BLOCK' || upper === 'WARN' || upper === 'ALLOW') return upper;
|
|
86
|
+
if (upper === 'LOG') return 'WARN';
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Normalize a single finding into the internal format.
|
|
92
|
+
*
|
|
93
|
+
* @param {object} finding - Raw finding from any scanner tool
|
|
94
|
+
* @param {string} sourceTool - Tool that produced this finding (fallback if not on finding)
|
|
95
|
+
* @param {object} [options] - Options
|
|
96
|
+
* @param {boolean} [options.includeRaw] - Include original finding as `raw` field
|
|
97
|
+
* @returns {object} Normalized finding
|
|
98
|
+
*/
|
|
99
|
+
export function normalizeFinding(finding, sourceTool, options = {}) {
|
|
100
|
+
if (!finding || typeof finding !== 'object') {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const originalSeverity = finding.severity || null;
|
|
105
|
+
const mapped = SEVERITY_MAP[originalSeverity] || DEFAULT_SEVERITY;
|
|
106
|
+
|
|
107
|
+
const normalized = {
|
|
108
|
+
rule_id: extractRuleId(finding) || 'unknown',
|
|
109
|
+
original_severity: originalSeverity,
|
|
110
|
+
severity: mapped.severity,
|
|
111
|
+
severity_rank: mapped.severity_rank,
|
|
112
|
+
confidence: normalizeConfidence(finding.confidence),
|
|
113
|
+
message: finding.message || '',
|
|
114
|
+
category: finding.category || inferCategory(extractRuleId(finding)),
|
|
115
|
+
cwe: finding.cwe || (finding.metadata && finding.metadata.cwe) || null,
|
|
116
|
+
owasp: finding.owasp || (finding.metadata && finding.metadata.owasp) || null,
|
|
117
|
+
file: finding.file || null,
|
|
118
|
+
line: typeof finding.line === 'number' ? finding.line : null,
|
|
119
|
+
action: normalizeAction(finding.action),
|
|
120
|
+
risk_score: typeof finding.risk_score === 'number'
|
|
121
|
+
? finding.risk_score
|
|
122
|
+
: (typeof finding.risk_score === 'string' ? parseFloat(finding.risk_score) || null : null),
|
|
123
|
+
source_tool: finding.source_tool || sourceTool || 'unknown',
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (options.includeRaw) {
|
|
127
|
+
normalized.raw = finding;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return normalized;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Normalize an array of findings.
|
|
135
|
+
*
|
|
136
|
+
* @param {object[]} findings - Array of raw findings
|
|
137
|
+
* @param {string} sourceTool - Default source tool (per-finding source_tool wins)
|
|
138
|
+
* @param {object} [options] - Options passed to normalizeFinding
|
|
139
|
+
* @returns {object[]} Array of normalized findings
|
|
140
|
+
*/
|
|
141
|
+
export function normalizeFindings(findings, sourceTool, options = {}) {
|
|
142
|
+
if (!Array.isArray(findings)) return [];
|
|
143
|
+
return findings
|
|
144
|
+
.map(f => normalizeFinding(f, sourceTool, options))
|
|
145
|
+
.filter(Boolean);
|
|
146
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/tools/compliance-controls.js — get_compliance_controls MCP tool (thin wrapper)
|
|
2
|
+
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { loadControls, filterControls } from '../lib/compliance-controls.js';
|
|
5
|
+
|
|
6
|
+
export const complianceControlsSchema = {
|
|
7
|
+
domain: z.enum(['security', 'safety', 'all']).optional().describe("Filter by domain"),
|
|
8
|
+
control_ids: z.array(z.string()).optional().describe("Specific control IDs to retrieve"),
|
|
9
|
+
owasp_filter: z.array(z.string()).optional().describe("Filter by OWASP LLM tags (e.g. LLM01)"),
|
|
10
|
+
verbosity: z.enum(['minimal', 'compact', 'full']).optional().describe("Response detail level"),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function getComplianceControls({ domain, control_ids, owasp_filter, verbosity }) {
|
|
14
|
+
const level = verbosity || 'compact';
|
|
15
|
+
|
|
16
|
+
const controls = filterControls({
|
|
17
|
+
domain: domain || 'all',
|
|
18
|
+
controlIds: control_ids,
|
|
19
|
+
owaspFilter: owasp_filter,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const registry = loadControls();
|
|
23
|
+
|
|
24
|
+
let output;
|
|
25
|
+
switch (level) {
|
|
26
|
+
case 'minimal':
|
|
27
|
+
output = {
|
|
28
|
+
framework: registry.framework,
|
|
29
|
+
controls_count: controls.length,
|
|
30
|
+
controls: controls.map(c => ({ id: c.id, title: c.title, domain: c.domain })),
|
|
31
|
+
};
|
|
32
|
+
break;
|
|
33
|
+
|
|
34
|
+
case 'full':
|
|
35
|
+
output = {
|
|
36
|
+
framework: registry.framework,
|
|
37
|
+
schema_version: registry.schema_version,
|
|
38
|
+
source: registry.source,
|
|
39
|
+
source_snapshot: registry.source_snapshot,
|
|
40
|
+
controls_count: controls.length,
|
|
41
|
+
controls,
|
|
42
|
+
};
|
|
43
|
+
break;
|
|
44
|
+
|
|
45
|
+
case 'compact':
|
|
46
|
+
default:
|
|
47
|
+
output = {
|
|
48
|
+
framework: registry.framework,
|
|
49
|
+
controls_count: controls.length,
|
|
50
|
+
controls: controls.map(c => ({
|
|
51
|
+
id: c.id,
|
|
52
|
+
title: c.title,
|
|
53
|
+
domain: c.domain,
|
|
54
|
+
owasp_llm: c.owasp_llm,
|
|
55
|
+
scanner_tools: c.scanner_tools,
|
|
56
|
+
evaluation: c.evaluation,
|
|
57
|
+
})),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
content: [{
|
|
63
|
+
type: 'text',
|
|
64
|
+
text: JSON.stringify(output, null, 2),
|
|
65
|
+
}],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// src/tools/score-aivss.js — score_aivss MCP tool (thin wrapper around src/lib/aivss.js)
|
|
2
|
+
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { normalizeFindings } from '../lib/normalize-finding.js';
|
|
5
|
+
import { scoreBatch, AIVSS_MODEL } from '../lib/aivss.js';
|
|
6
|
+
|
|
7
|
+
export const scoreAivssSchema = {
|
|
8
|
+
findings: z.array(z.object({
|
|
9
|
+
ruleId: z.string().optional(),
|
|
10
|
+
rule_id: z.string().optional(),
|
|
11
|
+
id: z.string().optional(),
|
|
12
|
+
rule: z.string().optional(),
|
|
13
|
+
severity: z.string(),
|
|
14
|
+
message: z.string(),
|
|
15
|
+
confidence: z.string().optional(),
|
|
16
|
+
category: z.string().optional(),
|
|
17
|
+
cwe: z.string().optional(),
|
|
18
|
+
owasp: z.string().optional(),
|
|
19
|
+
risk_score: z.union([z.number(), z.string()]).optional(),
|
|
20
|
+
action: z.string().optional(),
|
|
21
|
+
file: z.string().optional(),
|
|
22
|
+
line: z.number().optional(),
|
|
23
|
+
source_tool: z.string().optional(),
|
|
24
|
+
})).describe('Findings from any scanner tool or raw JSON'),
|
|
25
|
+
source_tool: z.string().optional().describe('Tool that produced findings, for normalization hints'),
|
|
26
|
+
overrides: z.object({
|
|
27
|
+
AV: z.string().optional(),
|
|
28
|
+
AC: z.string().optional(),
|
|
29
|
+
PR: z.string().optional(),
|
|
30
|
+
UI: z.string().optional(),
|
|
31
|
+
S: z.string().optional(),
|
|
32
|
+
MR: z.string().optional(),
|
|
33
|
+
DS: z.string().optional(),
|
|
34
|
+
EI: z.string().optional(),
|
|
35
|
+
DC: z.string().optional(),
|
|
36
|
+
AD: z.string().optional(),
|
|
37
|
+
C: z.string().optional(),
|
|
38
|
+
I: z.string().optional(),
|
|
39
|
+
A: z.string().optional(),
|
|
40
|
+
SI: z.string().optional(),
|
|
41
|
+
}).optional().describe('Manual AIVSS metric overrides'),
|
|
42
|
+
verbosity: z.enum(['minimal', 'compact', 'full']).optional().describe("Response detail level"),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export async function scoreAivssTool({ findings, source_tool, overrides, verbosity }) {
|
|
46
|
+
const level = verbosity || 'compact';
|
|
47
|
+
|
|
48
|
+
const normalized = normalizeFindings(findings, source_tool || 'unknown', {
|
|
49
|
+
includeRaw: level === 'full',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const result = scoreBatch(normalized, overrides || {});
|
|
53
|
+
|
|
54
|
+
let output;
|
|
55
|
+
switch (level) {
|
|
56
|
+
case 'minimal':
|
|
57
|
+
output = {
|
|
58
|
+
findings_count: result.findings.length,
|
|
59
|
+
posture_score: result.posture.posture_score,
|
|
60
|
+
posture_rating: result.posture.posture_rating,
|
|
61
|
+
aggregate_method: result.posture.aggregate_method,
|
|
62
|
+
};
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case 'full':
|
|
66
|
+
output = {
|
|
67
|
+
findings: result.findings,
|
|
68
|
+
posture: result.posture,
|
|
69
|
+
};
|
|
70
|
+
break;
|
|
71
|
+
|
|
72
|
+
case 'compact':
|
|
73
|
+
default:
|
|
74
|
+
output = {
|
|
75
|
+
findings: result.findings.map(f => ({
|
|
76
|
+
rule_id: f.rule_id,
|
|
77
|
+
aivss_score: f.aivss_score,
|
|
78
|
+
rating: f.rating,
|
|
79
|
+
vector_string: f.vector_string,
|
|
80
|
+
mapping_confidence: f.metrics.mapping_confidence,
|
|
81
|
+
})),
|
|
82
|
+
posture: {
|
|
83
|
+
posture_score: result.posture.posture_score,
|
|
84
|
+
posture_rating: result.posture.posture_rating,
|
|
85
|
+
max_score: result.posture.max_score,
|
|
86
|
+
score_distribution: result.posture.score_distribution,
|
|
87
|
+
aggregate_method: result.posture.aggregate_method,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
content: [{
|
|
94
|
+
type: 'text',
|
|
95
|
+
text: JSON.stringify(output, null, 2),
|
|
96
|
+
}],
|
|
97
|
+
};
|
|
98
|
+
}
|