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.
@@ -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
+ }