praxis-sec 1.0.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 +21 -0
- package/README.md +170 -0
- package/ai-defense/cost-protection.md +292 -0
- package/ai-defense/llm-security-checklist.md +324 -0
- package/ai-defense/prompt-injection-patterns.js +283 -0
- package/ai-defense/system-prompt-armor.md +327 -0
- package/checklists/launch-day.md +168 -0
- package/cli/agents/abom-generator.js +225 -0
- package/cli/agents/agent-attestation-agent.js +318 -0
- package/cli/agents/agent-config-scanner.js +787 -0
- package/cli/agents/agent-telemetry-agent.js +415 -0
- package/cli/agents/agentic-security-agent.js +296 -0
- package/cli/agents/agentic-supply-chain-agent.js +463 -0
- package/cli/agents/ai-infra-inventory-agent.js +449 -0
- package/cli/agents/api-fuzzer.js +345 -0
- package/cli/agents/auth-bypass-agent.js +348 -0
- package/cli/agents/base-agent.js +280 -0
- package/cli/agents/cicd-scanner.js +300 -0
- package/cli/agents/config-auditor.js +757 -0
- package/cli/agents/deep-analyzer.js +776 -0
- package/cli/agents/endpoint-agent-abuse-agent.js +404 -0
- package/cli/agents/exception-handler-agent.js +187 -0
- package/cli/agents/git-history-scanner.js +169 -0
- package/cli/agents/governance-audits.js +138 -0
- package/cli/agents/hermes-security-agent.js +536 -0
- package/cli/agents/html-reporter.js +1125 -0
- package/cli/agents/index.js +147 -0
- package/cli/agents/injection-tester.js +502 -0
- package/cli/agents/legal-risk-agent.js +328 -0
- package/cli/agents/llm-redteam.js +199 -0
- package/cli/agents/managed-agent-scanner.js +333 -0
- package/cli/agents/mcp-security-agent.js +588 -0
- package/cli/agents/memory-poisoning-agent.js +305 -0
- package/cli/agents/mobile-scanner.js +231 -0
- package/cli/agents/model-file-scanner.js +259 -0
- package/cli/agents/orchestrator.js +355 -0
- package/cli/agents/pii-compliance-agent.js +301 -0
- package/cli/agents/policy-engine.js +229 -0
- package/cli/agents/prompt-injection-prober.js +224 -0
- package/cli/agents/rag-security-agent.js +204 -0
- package/cli/agents/recon-agent.js +207 -0
- package/cli/agents/sbom-generator.js +265 -0
- package/cli/agents/scoring-engine.js +273 -0
- package/cli/agents/ssrf-prober.js +130 -0
- package/cli/agents/stateful-watcher.js +238 -0
- package/cli/agents/supabase-rls-agent.js +154 -0
- package/cli/agents/supply-chain-agent.js +857 -0
- package/cli/agents/swarm-orchestrator.js +200 -0
- package/cli/agents/verifier-agent.js +303 -0
- package/cli/agents/vibe-coding-agent.js +250 -0
- package/cli/bin/praxis.js +866 -0
- package/cli/commands/abom.js +73 -0
- package/cli/commands/agent-fix.js +1245 -0
- package/cli/commands/audit.js +1180 -0
- package/cli/commands/autofix.js +383 -0
- package/cli/commands/baseline.js +193 -0
- package/cli/commands/benchmark.js +327 -0
- package/cli/commands/checklist.js +223 -0
- package/cli/commands/ci.js +403 -0
- package/cli/commands/deps.js +516 -0
- package/cli/commands/diff.js +200 -0
- package/cli/commands/doctor.js +195 -0
- package/cli/commands/env-audit.js +349 -0
- package/cli/commands/fix.js +218 -0
- package/cli/commands/guard.js +396 -0
- package/cli/commands/hooks.js +278 -0
- package/cli/commands/init.js +514 -0
- package/cli/commands/legal.js +158 -0
- package/cli/commands/live-advisories.js +241 -0
- package/cli/commands/mcp.js +660 -0
- package/cli/commands/openclaw.js +386 -0
- package/cli/commands/red-team.js +350 -0
- package/cli/commands/redteam.js +78 -0
- package/cli/commands/remediate.js +797 -0
- package/cli/commands/rotate.js +768 -0
- package/cli/commands/rules.js +196 -0
- package/cli/commands/scan-mcp.js +534 -0
- package/cli/commands/scan-skill.js +588 -0
- package/cli/commands/scan-standard.js +251 -0
- package/cli/commands/scan.js +524 -0
- package/cli/commands/score.js +449 -0
- package/cli/commands/shell.js +514 -0
- package/cli/commands/team-report.js +398 -0
- package/cli/commands/undo.js +161 -0
- package/cli/commands/update-intel.js +126 -0
- package/cli/commands/vibe-check.js +276 -0
- package/cli/commands/watch.js +757 -0
- package/cli/commands/web.js +63 -0
- package/cli/core/ast/guardrail-detector.js +141 -0
- package/cli/core/ast/index.js +22 -0
- package/cli/core/ast/parser.js +676 -0
- package/cli/core/ast/scope-tree.js +287 -0
- package/cli/core/ast/taint-tracker.js +158 -0
- package/cli/core/branding.js +37 -0
- package/cli/core/env.js +38 -0
- package/cli/core/errors.js +61 -0
- package/cli/core/fs.js +62 -0
- package/cli/core/output/compliance.js +90 -0
- package/cli/core/output/html-theme.js +158 -0
- package/cli/core/output/index.js +57 -0
- package/cli/core/output/json.js +48 -0
- package/cli/core/output/sarif.js +240 -0
- package/cli/core/version.js +67 -0
- package/cli/core/web/jobs.js +183 -0
- package/cli/core/web/projects.js +146 -0
- package/cli/core/web/server.js +439 -0
- package/cli/data/atlas-knowledge.json +5640 -0
- package/cli/data/eaa-catalog.json +39 -0
- package/cli/data/known-mcps.json +26 -0
- package/cli/data/probes/prompt-injection-corpus.json +271 -0
- package/cli/data/threat-intel.json +85 -0
- package/cli/data/threatpacks/latest.json +41 -0
- package/cli/hooks/patterns.js +313 -0
- package/cli/hooks/post-tool-use.js +140 -0
- package/cli/hooks/pre-tool-use.js +186 -0
- package/cli/index.js +90 -0
- package/cli/providers/llm-provider.js +766 -0
- package/cli/utils/autofix-rules.js +74 -0
- package/cli/utils/cache-manager.js +310 -0
- package/cli/utils/compliance-map.js +191 -0
- package/cli/utils/entropy.js +132 -0
- package/cli/utils/fix-ledger.js +127 -0
- package/cli/utils/hermes-tool-registry.js +252 -0
- package/cli/utils/intel/cache.js +61 -0
- package/cli/utils/intel/http.js +88 -0
- package/cli/utils/intel/index.js +235 -0
- package/cli/utils/intel/merge.js +229 -0
- package/cli/utils/intel/sources/epss.js +54 -0
- package/cli/utils/intel/sources/ghsa.js +81 -0
- package/cli/utils/intel/sources/gitguardian.js +40 -0
- package/cli/utils/intel/sources/gitleaks.js +101 -0
- package/cli/utils/intel/sources/kev.js +38 -0
- package/cli/utils/intel/sources/nvd.js +84 -0
- package/cli/utils/intel/sources/osv.js +132 -0
- package/cli/utils/intel/sources/phylum.js +44 -0
- package/cli/utils/intel/sources/snyk.js +46 -0
- package/cli/utils/intel/sources/socket.js +69 -0
- package/cli/utils/intel/sources/sonatype.js +84 -0
- package/cli/utils/intel/sources/threatpack.js +69 -0
- package/cli/utils/mcp-trust.js +60 -0
- package/cli/utils/output.js +251 -0
- package/cli/utils/patterns.js +1130 -0
- package/cli/utils/pdf-generator.js +94 -0
- package/cli/utils/plugin-loader.js +364 -0
- package/cli/utils/rule-import.js +228 -0
- package/cli/utils/rule-registry.js +426 -0
- package/cli/utils/scan-fingerprint.js +109 -0
- package/cli/utils/scan-playbook.js +312 -0
- package/cli/utils/score-history.js +119 -0
- package/cli/utils/secrets-verifier.js +247 -0
- package/cli/utils/security-memory.js +296 -0
- package/cli/utils/standards/atlas-knowledge.js +87 -0
- package/cli/utils/standards/index.js +127 -0
- package/cli/utils/standards/sources/avid.js +45 -0
- package/cli/utils/standards/sources/eu-ai-act.js +89 -0
- package/cli/utils/standards/sources/google-saif.js +39 -0
- package/cli/utils/standards/sources/iso-42001.js +94 -0
- package/cli/utils/standards/sources/mitre-atlas.js +54 -0
- package/cli/utils/standards/sources/nist-ai-600-1.js +45 -0
- package/cli/utils/standards/sources/owasp-llm.js +45 -0
- package/cli/utils/standards/sources/owasp-ml.js +45 -0
- package/cli/utils/threat-intel.js +265 -0
- package/configs/firebase/firestore-rules.txt +215 -0
- package/configs/firebase/security-checklist.md +236 -0
- package/configs/firebase/storage-rules.txt +206 -0
- package/configs/gitignore-template +258 -0
- package/configs/nextjs-security-headers.js +220 -0
- package/configs/praxisignore-template +50 -0
- package/configs/supabase/secure-client.ts +225 -0
- package/configs/supabase/security-checklist.md +278 -0
- package/docs/THIRD_PARTY_NOTICES.md +26 -0
- package/docs/THREAT_INTEL.md +292 -0
- package/docs/USAGE.md +1205 -0
- package/docs/design/WEB-UI.md +82 -0
- package/package.json +71 -0
- package/scripts/check-determinism.mjs +119 -0
- package/snippets/README.md +122 -0
- package/snippets/api-security/api-security-checklist.md +412 -0
- package/snippets/api-security/cors-config.ts +322 -0
- package/snippets/api-security/input-validation.ts +430 -0
- package/snippets/auth/jwt-checklist.md +322 -0
- package/snippets/rate-limiting/nextjs-middleware.ts +211 -0
- package/snippets/rate-limiting/upstash-ratelimit.ts +229 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SwarmOrchestrator — K2.6-Powered Parallel Security Swarm
|
|
3
|
+
* ==========================================================
|
|
4
|
+
*
|
|
5
|
+
* Instead of running 28 agents locally in Node.js (chunks of 6),
|
|
6
|
+
* --swarm sends the entire task to Kimi K2.6 and lets its native
|
|
7
|
+
* 300-agent swarm handle parallel analysis.
|
|
8
|
+
*
|
|
9
|
+
* Each of Praxis's 23 attack classes is assigned as an explicit
|
|
10
|
+
* sub-agent role. K2.6 fans out, each sub-agent scans for its class,
|
|
11
|
+
* and results are returned as a consolidated findings array.
|
|
12
|
+
*
|
|
13
|
+
* Output is mapped back to Praxis's Finding format so SARIF,
|
|
14
|
+
* HTML reports, and CI exit codes work unchanged.
|
|
15
|
+
*
|
|
16
|
+
* USAGE:
|
|
17
|
+
* npx praxis-sec red-team . --swarm
|
|
18
|
+
* npx praxis-sec red-team . --swarm --provider kimi
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from 'fs';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import { createProvider, autoDetectProvider } from '../providers/llm-provider.js';
|
|
24
|
+
import { ReconAgent } from './recon-agent.js';
|
|
25
|
+
import { createFinding } from './base-agent.js';
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// AGENT ROLE DEFINITIONS — maps Praxis's 23 attack classes to swarm roles
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
const SWARM_ROLES = [
|
|
32
|
+
{ id: 'injection', name: 'Injection Tester', desc: 'SQL injection, command injection, LDAP injection, XPath injection, template injection' },
|
|
33
|
+
{ id: 'auth-bypass', name: 'Auth Bypass Agent', desc: 'Authentication bypass, authorization flaws, privilege escalation, JWT weaknesses' },
|
|
34
|
+
{ id: 'ssrf', name: 'SSRF Prober', desc: 'Server-side request forgery, SSRF via redirects, internal service exposure' },
|
|
35
|
+
{ id: 'supply-chain', name: 'Supply Chain Auditor', desc: 'Dependency confusion, typosquatting, malicious packages, outdated deps with CVEs' },
|
|
36
|
+
{ id: 'config', name: 'Config Auditor', desc: 'Hardcoded secrets, insecure defaults, exposed debug endpoints, misconfigured CORS' },
|
|
37
|
+
{ id: 'llm-redteam', name: 'LLM Red Team', desc: 'Prompt injection, jailbreaks, unsafe LLM output rendering, model inversion' },
|
|
38
|
+
{ id: 'mobile', name: 'Mobile Scanner', desc: 'Insecure data storage, weak crypto, insecure communication, exported components' },
|
|
39
|
+
{ id: 'git-history', name: 'Git History Scanner', desc: 'Secrets committed in git history, deleted files with sensitive data' },
|
|
40
|
+
{ id: 'cicd', name: 'CI/CD Scanner', desc: 'Insecure GitHub Actions, exposed secrets in workflows, artifact poisoning' },
|
|
41
|
+
{ id: 'api-fuzzer', name: 'API Fuzzer', desc: 'Missing input validation, mass assignment, insecure direct object references (IDOR)' },
|
|
42
|
+
{ id: 'supabase-rls', name: 'Supabase RLS Agent', desc: 'Missing row-level security, exposed Supabase service keys, insecure RLS policies' },
|
|
43
|
+
{ id: 'mcp-security', name: 'MCP Security Agent', desc: 'Tool poisoning, MCP server misconfiguration, unsafe tool definitions' },
|
|
44
|
+
{ id: 'agentic-security', name: 'Agentic Security Agent', desc: 'Agentic loop vulnerabilities, unsafe tool use, context window attacks' },
|
|
45
|
+
{ id: 'rag-security', name: 'RAG Security Agent', desc: 'Prompt injection via retrieved documents, data poisoning, retrieval manipulation' },
|
|
46
|
+
{ id: 'pii-compliance', name: 'PII Compliance Agent', desc: 'PII exposure, GDPR/CCPA violations, unencrypted personal data' },
|
|
47
|
+
{ id: 'vibe-coding', name: 'Vibe Coding Agent', desc: 'AI-generated code security issues, hardcoded values from iterative prompting' },
|
|
48
|
+
{ id: 'exception-handler', name: 'Exception Handler Agent', desc: 'Stack traces in responses, error information disclosure, unhandled exceptions' },
|
|
49
|
+
{ id: 'agent-config', name: 'Agent Config Scanner', desc: 'Insecure agent config files (.cursorrules, CLAUDE.md, MCP configs)' },
|
|
50
|
+
{ id: 'memory-poisoning', name: 'Memory Poisoning Agent', desc: 'Malicious content in AI memory stores, embedding poisoning' },
|
|
51
|
+
{ id: 'managed-agent', name: 'Managed Agent Scanner', desc: 'Insecure managed agent platforms, overprivileged agents' },
|
|
52
|
+
{ id: 'hermes-security', name: 'Hermes Security Agent', desc: 'Hermes CLI security, agent tool permissions, orchestrator misconfiguration' },
|
|
53
|
+
{ id: 'agent-attestation', name: 'Agent Attestation Agent', desc: 'Missing agent identity verification, unauthenticated agent-to-agent calls' },
|
|
54
|
+
{ id: 'agentic-supply-chain', name: 'Agentic Supply Chain Agent', desc: 'Compromised AI integrations, OAuth scope creep, MCP server supply chain' },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// Max file content to include in the swarm prompt (cost control)
|
|
58
|
+
const MAX_FILE_CHARS = 200_000;
|
|
59
|
+
const MAX_FILES = 100;
|
|
60
|
+
|
|
61
|
+
// =============================================================================
|
|
62
|
+
// SWARM ORCHESTRATOR
|
|
63
|
+
// =============================================================================
|
|
64
|
+
|
|
65
|
+
export class SwarmOrchestrator {
|
|
66
|
+
/**
|
|
67
|
+
* @param {object} options
|
|
68
|
+
* @param {object} options.provider — LLM provider (must be Kimi or OpenAI-compatible with tool use)
|
|
69
|
+
* @param {boolean} options.verbose
|
|
70
|
+
* @param {number} options.budgetCents
|
|
71
|
+
*/
|
|
72
|
+
constructor(options = {}) {
|
|
73
|
+
this.provider = options.provider;
|
|
74
|
+
this.verbose = options.verbose || false;
|
|
75
|
+
this.budgetCents = options.budgetCents ?? 200;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static create(rootPath, options = {}) {
|
|
79
|
+
if (typeof options.provider === 'string') {
|
|
80
|
+
// Explicit provider requested
|
|
81
|
+
const provider = autoDetectProvider(rootPath, { provider: options.provider, model: options.model });
|
|
82
|
+
if (!provider) return null;
|
|
83
|
+
return new SwarmOrchestrator({ provider, verbose: options.verbose, budgetCents: options.budgetCents });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Auto-select: prefer deepseek-flash (1M ctx, cheap) then kimi as fallback
|
|
87
|
+
for (const [providerName, swarmModel] of [
|
|
88
|
+
['deepseek-flash', 'deepseek-v4-flash'],
|
|
89
|
+
['kimi', 'moonshot-v1-128k'],
|
|
90
|
+
]) {
|
|
91
|
+
const provider = autoDetectProvider(rootPath, { provider: providerName, model: swarmModel });
|
|
92
|
+
if (provider) return new SwarmOrchestrator({ provider, verbose: options.verbose, budgetCents: options.budgetCents });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Run the swarm scan against a codebase.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} rootPath
|
|
102
|
+
* @param {object} reconData — Output from ReconAgent
|
|
103
|
+
* @param {string[]} files — All scannable files
|
|
104
|
+
* @returns {Promise<object[]>} — findings[]
|
|
105
|
+
*/
|
|
106
|
+
async run(rootPath, reconData, files) {
|
|
107
|
+
const codeBundle = this._bundleCode(rootPath, files);
|
|
108
|
+
const prompt = this._buildSwarmPrompt(reconData, codeBundle, rootPath);
|
|
109
|
+
|
|
110
|
+
const systemPrompt = `You are a security swarm coordinator. You MUST respond with ONLY a valid JSON object — no prose, no markdown, no explanation, no code fences. Your response must start with { and end with }. Deploy all ${SWARM_ROLES.length} sub-agents, each scanning for their attack class, then output the consolidated JSON findings.`;
|
|
111
|
+
|
|
112
|
+
const jsonInstruction = '\n\nOutput a JSON object with exactly these keys: {"findings":[{"agentId":"<agent-id>","file":"<relative-path>","line":<number>,"severity":"critical|high|medium|low","rule":"<rule-id>","title":"<title>","description":"<description>","remediation":"<fix>"}],"agentSummary":[{"agentId":"<agent-id>","findingCount":<number>,"status":"clean|findings"}]}';
|
|
113
|
+
|
|
114
|
+
const text = await this.provider.complete(systemPrompt, prompt + jsonInstruction, { maxTokens: 8192, jsonMode: true });
|
|
115
|
+
let raw = null;
|
|
116
|
+
try {
|
|
117
|
+
raw = JSON.parse(text || '{}');
|
|
118
|
+
} catch {
|
|
119
|
+
if (this.verbose) console.log(' [Swarm] JSON parse failed. Preview:', text?.slice(0, 200));
|
|
120
|
+
raw = null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return this._mapFindings(raw?.findings ?? [], rootPath);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
_bundleCode(rootPath, files) {
|
|
127
|
+
let bundle = '';
|
|
128
|
+
let totalChars = 0;
|
|
129
|
+
const selected = files.slice(0, MAX_FILES);
|
|
130
|
+
|
|
131
|
+
for (const filePath of selected) {
|
|
132
|
+
if (totalChars >= MAX_FILE_CHARS) break;
|
|
133
|
+
try {
|
|
134
|
+
const relPath = path.relative(rootPath, filePath);
|
|
135
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
136
|
+
const snippet = content.slice(0, Math.min(8000, MAX_FILE_CHARS - totalChars));
|
|
137
|
+
bundle += `\n\n### ${relPath}\n\`\`\`\n${snippet}\n\`\`\``;
|
|
138
|
+
totalChars += snippet.length;
|
|
139
|
+
} catch { /* skip unreadable */ }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return bundle;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_buildSwarmPrompt(recon, codeBundle, rootPath) {
|
|
146
|
+
const projectName = path.basename(rootPath);
|
|
147
|
+
const reconSummary = recon
|
|
148
|
+
? [
|
|
149
|
+
recon.frameworks?.length ? `Frameworks: ${recon.frameworks.join(', ')}` : '',
|
|
150
|
+
recon.databases?.length ? `Databases: ${recon.databases.join(', ')}` : '',
|
|
151
|
+
recon.authPatterns?.length ? `Auth patterns: ${recon.authPatterns.join(', ')}` : '',
|
|
152
|
+
recon.languages?.length ? `Languages: ${recon.languages.join(', ')}` : '',
|
|
153
|
+
].filter(Boolean).join('\n')
|
|
154
|
+
: '';
|
|
155
|
+
|
|
156
|
+
const agentList = SWARM_ROLES.map((r, i) =>
|
|
157
|
+
` Sub-agent ${String(i + 1).padStart(2, '0')} [${r.id}] — ${r.name}: ${r.desc}`
|
|
158
|
+
).join('\n');
|
|
159
|
+
|
|
160
|
+
return `# Security Swarm Task: ${projectName}
|
|
161
|
+
|
|
162
|
+
## Project Context
|
|
163
|
+
${reconSummary || 'No recon data available.'}
|
|
164
|
+
|
|
165
|
+
## Sub-Agent Assignments
|
|
166
|
+
Deploy all ${SWARM_ROLES.length} sub-agents in parallel. Each scans for exactly their assigned attack class:
|
|
167
|
+
|
|
168
|
+
${agentList}
|
|
169
|
+
|
|
170
|
+
## Instructions
|
|
171
|
+
1. Each sub-agent independently analyzes the full codebase for its attack class.
|
|
172
|
+
2. For each finding, record: agentId (the sub-agent's id), file path, line number, severity, a rule identifier, title, description, the matched snippet, and remediation advice.
|
|
173
|
+
3. Severity scale: critical (exploitable now), high (likely exploitable), medium (potential issue), low (best practice), info (note).
|
|
174
|
+
4. Report all findings from all sub-agents in the tool call, even if the list is long.
|
|
175
|
+
5. If a sub-agent finds nothing, include it in agentSummary with status "clean" and findingCount 0.
|
|
176
|
+
|
|
177
|
+
## Codebase
|
|
178
|
+
${codeBundle}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
_mapFindings(rawFindings, rootPath) {
|
|
182
|
+
return rawFindings.map(r => {
|
|
183
|
+
const role = SWARM_ROLES.find(a => a.id === r.agentId) || { name: 'SwarmAgent', id: r.agentId };
|
|
184
|
+
return createFinding({
|
|
185
|
+
file: r.file ? path.resolve(rootPath, r.file) : null,
|
|
186
|
+
line: r.line || 0,
|
|
187
|
+
severity: r.severity || 'medium',
|
|
188
|
+
confidence: 'medium',
|
|
189
|
+
rule: r.rule || `swarm:${role.id}`,
|
|
190
|
+
title: r.title,
|
|
191
|
+
description: r.description,
|
|
192
|
+
matched: r.matched || '',
|
|
193
|
+
remediation: r.remediation || '',
|
|
194
|
+
category: role.name,
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export default SwarmOrchestrator;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VerifierAgent — Second-Pass Finding Confirmation
|
|
3
|
+
* ==================================================
|
|
4
|
+
*
|
|
5
|
+
* Runs after all agents complete. Takes high-confidence findings
|
|
6
|
+
* and attempts to confirm or downgrade them by analyzing surrounding
|
|
7
|
+
* code context.
|
|
8
|
+
*
|
|
9
|
+
* Checks:
|
|
10
|
+
* - Is the flagged value static/hardcoded or dynamic (from user input)?
|
|
11
|
+
* - Is there upstream sanitization or validation?
|
|
12
|
+
* - Is the code inside error handling that neutralizes it?
|
|
13
|
+
* - Is the finding in dead/unreachable code?
|
|
14
|
+
*
|
|
15
|
+
* Impact: Unverified findings get downgraded one confidence level.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { ASTParser, ScopeTree, TaintTracker, GuardrailDetector } from '../core/ast/index.js';
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// HEURISTIC PATTERNS
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
/** Sources of user input — if a finding's matched code references these, it's more likely real */
|
|
27
|
+
const USER_INPUT_SOURCES = [
|
|
28
|
+
/req\.body/,
|
|
29
|
+
/req\.query/,
|
|
30
|
+
/req\.params/,
|
|
31
|
+
/req\.headers/,
|
|
32
|
+
/request\.body/,
|
|
33
|
+
/request\.query/,
|
|
34
|
+
/request\.params/,
|
|
35
|
+
/request\.form/,
|
|
36
|
+
/request\.args/,
|
|
37
|
+
/request\.json/,
|
|
38
|
+
/ctx\.request/,
|
|
39
|
+
/ctx\.query/,
|
|
40
|
+
/ctx\.params/,
|
|
41
|
+
/event\.body/,
|
|
42
|
+
/event\.queryStringParameters/,
|
|
43
|
+
/searchParams/,
|
|
44
|
+
/formData/,
|
|
45
|
+
/userinput/i,
|
|
46
|
+
/user_input/i,
|
|
47
|
+
/input\s*\(/,
|
|
48
|
+
/argv/,
|
|
49
|
+
/process\.env/,
|
|
50
|
+
/getenv/,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/** Sanitization/validation indicators — presence near a finding suggests it's protected */
|
|
54
|
+
export const SANITIZATION_PATTERNS = [
|
|
55
|
+
/sanitize/i,
|
|
56
|
+
/validate/i,
|
|
57
|
+
/escape/i,
|
|
58
|
+
/purify/i,
|
|
59
|
+
/DOMPurify/,
|
|
60
|
+
/xss\s*\(/i,
|
|
61
|
+
/htmlencode/i,
|
|
62
|
+
/encodeURI/,
|
|
63
|
+
/encodeURIComponent/,
|
|
64
|
+
/parameterized/i,
|
|
65
|
+
/prepared\s*statement/i,
|
|
66
|
+
/placeholder/i,
|
|
67
|
+
/\?\s*,/,
|
|
68
|
+
/\$\d+/,
|
|
69
|
+
/bindParam/i,
|
|
70
|
+
/bindValue/i,
|
|
71
|
+
/zod/i,
|
|
72
|
+
/yup/i,
|
|
73
|
+
/joi\./i,
|
|
74
|
+
/ajv/i,
|
|
75
|
+
/schema\.parse/i,
|
|
76
|
+
/safeParse/i,
|
|
77
|
+
/validator\./i,
|
|
78
|
+
/parseInt\s*\(/,
|
|
79
|
+
/parseFloat\s*\(/,
|
|
80
|
+
/Number\s*\(/,
|
|
81
|
+
/\.trim\s*\(/,
|
|
82
|
+
/\.replace\s*\(/,
|
|
83
|
+
/allowlist/i,
|
|
84
|
+
/whitelist/i,
|
|
85
|
+
/blocklist/i,
|
|
86
|
+
/blacklist/i,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/** Error handling wrappers — findings inside these are less exploitable */
|
|
90
|
+
const ERROR_HANDLING_PATTERNS = [
|
|
91
|
+
/}\s*catch\s*\(/,
|
|
92
|
+
/\.catch\s*\(/,
|
|
93
|
+
/try\s*\{/,
|
|
94
|
+
/if\s*\(\s*err/,
|
|
95
|
+
/on\s*\(\s*['"]error['"]/,
|
|
96
|
+
/\.on\s*\(\s*['"]error['"]/,
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/** Static/hardcoded value indicators — finding uses a constant, not user input */
|
|
100
|
+
const STATIC_VALUE_PATTERNS = [
|
|
101
|
+
/['"][^'"]{0,200}['"]/,
|
|
102
|
+
/const\s+\w+\s*=\s*['"][^'"]*['"]/,
|
|
103
|
+
/^\s*\/\//,
|
|
104
|
+
/^\s*\*/,
|
|
105
|
+
/^\s*#/,
|
|
106
|
+
/TODO|FIXME|HACK|NOTE/,
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
/** Dead code indicators */
|
|
110
|
+
const DEAD_CODE_PATTERNS = [
|
|
111
|
+
/return\s+/,
|
|
112
|
+
/throw\s+/,
|
|
113
|
+
/process\.exit/,
|
|
114
|
+
/^\s*\/\//,
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
// =============================================================================
|
|
118
|
+
// VERIFIER AGENT
|
|
119
|
+
// =============================================================================
|
|
120
|
+
|
|
121
|
+
export class VerifierAgent {
|
|
122
|
+
constructor() {
|
|
123
|
+
this.name = 'VerifierAgent';
|
|
124
|
+
this.description = 'Second-pass verification of findings';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Verify an array of findings by analyzing surrounding code context.
|
|
129
|
+
* Returns findings with added `verified` and `verifierNote` fields.
|
|
130
|
+
*
|
|
131
|
+
* @param {object[]} findings — Findings from all agents (post-dedup)
|
|
132
|
+
* @param {object} options — { verbose }
|
|
133
|
+
* @returns {object[]} — Findings with verification metadata
|
|
134
|
+
*/
|
|
135
|
+
verify(findings, options = {}) {
|
|
136
|
+
const fileCache = new Map();
|
|
137
|
+
const astCache = new Map();
|
|
138
|
+
|
|
139
|
+
for (const finding of findings) {
|
|
140
|
+
// Only verify critical and high severity findings
|
|
141
|
+
if (finding.severity !== 'critical' && finding.severity !== 'high') {
|
|
142
|
+
finding.verified = null; // not checked
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const result = this._verifyFinding(finding, fileCache, astCache);
|
|
147
|
+
finding.verified = result.verified;
|
|
148
|
+
finding.verifierNote = result.note;
|
|
149
|
+
|
|
150
|
+
// Downgrade unverified findings one confidence level
|
|
151
|
+
if (!result.verified) {
|
|
152
|
+
if (finding.confidence === 'high') finding.confidence = 'medium';
|
|
153
|
+
else if (finding.confidence === 'medium') finding.confidence = 'low';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return findings;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Verify a single finding by reading surrounding code and running AST taint analysis.
|
|
162
|
+
*/
|
|
163
|
+
_verifyFinding(finding, fileCache, astCache = new Map()) {
|
|
164
|
+
const { file, line, matched } = finding;
|
|
165
|
+
if (!file || !line) {
|
|
166
|
+
return { verified: null, note: 'Missing file or line info' };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Read the file (cached)
|
|
170
|
+
let content;
|
|
171
|
+
let lines;
|
|
172
|
+
let scopeTree;
|
|
173
|
+
if (fileCache.has(file)) {
|
|
174
|
+
content = fileCache.get(file);
|
|
175
|
+
lines = content.split('\n');
|
|
176
|
+
scopeTree = astCache.get(file);
|
|
177
|
+
} else {
|
|
178
|
+
try {
|
|
179
|
+
content = fs.readFileSync(file, 'utf-8');
|
|
180
|
+
lines = content.split('\n');
|
|
181
|
+
fileCache.set(file, content);
|
|
182
|
+
|
|
183
|
+
const parsed = ASTParser.parse(content, file);
|
|
184
|
+
scopeTree = ScopeTree.build(parsed.ast, content);
|
|
185
|
+
astCache.set(file, scopeTree);
|
|
186
|
+
} catch {
|
|
187
|
+
return { verified: null, note: 'Could not read file' };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── AST Step 1: AI Guardrail / Defense framework check ──────────────────
|
|
192
|
+
const protection = GuardrailDetector.checkProtection(finding, content);
|
|
193
|
+
if (protection.isProtected) {
|
|
194
|
+
return {
|
|
195
|
+
verified: false,
|
|
196
|
+
note: `Mitigated: ${protection.reason}`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── AST Step 2: Dataflow & Taint Tracker Evaluation ──────────────────────
|
|
201
|
+
const taintEval = TaintTracker.evaluateFinding({
|
|
202
|
+
file,
|
|
203
|
+
line,
|
|
204
|
+
matched,
|
|
205
|
+
code: content,
|
|
206
|
+
scopeTree,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (taintEval.isSanitized) {
|
|
210
|
+
return {
|
|
211
|
+
verified: false,
|
|
212
|
+
note: taintEval.reason || 'Sanitization detected in AST scope',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (taintEval.isStatic) {
|
|
217
|
+
return {
|
|
218
|
+
verified: false,
|
|
219
|
+
note: taintEval.reason || 'Value appears to be static constant',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Get scope boundaries
|
|
224
|
+
const scope = scopeTree ? scopeTree.getScopeAt(line) : null;
|
|
225
|
+
const startLine = scope ? Math.max(0, scope.range.startLine - 1) : Math.max(0, line - 16);
|
|
226
|
+
const endLine = scope ? Math.min(lines.length, scope.range.endLine) : Math.min(lines.length, line + 15);
|
|
227
|
+
const windowText = lines.slice(startLine, endLine).join('\n');
|
|
228
|
+
const beforeText = lines.slice(startLine, Math.max(startLine, line - 1)).join('\n');
|
|
229
|
+
const findingLine = lines[line - 1] || '';
|
|
230
|
+
|
|
231
|
+
// ── Check 3: Is dead code? ──────────────────────────────────────────────
|
|
232
|
+
const isDeadCode = this._isDeadCode(lines, line);
|
|
233
|
+
if (isDeadCode) {
|
|
234
|
+
return {
|
|
235
|
+
verified: false,
|
|
236
|
+
note: 'Finding appears to be in unreachable code (after return/throw)',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Check 4: Error handling wrapper ─────────────────────────────────────
|
|
241
|
+
const inErrorHandler = ERROR_HANDLING_PATTERNS.some(p => p.test(beforeText));
|
|
242
|
+
if (inErrorHandler) {
|
|
243
|
+
return {
|
|
244
|
+
verified: false,
|
|
245
|
+
note: 'Finding is inside error handling context, reducing exploitability',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Check 5: Confirmed user input ───────────────────────────────────────
|
|
250
|
+
if (taintEval.isTainted || USER_INPUT_SOURCES.some(p => p.test(windowText))) {
|
|
251
|
+
return {
|
|
252
|
+
verified: true,
|
|
253
|
+
note: 'User input flows to this sink within enclosing scope',
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Default: cannot determine, keep as-is
|
|
258
|
+
return {
|
|
259
|
+
verified: null,
|
|
260
|
+
note: 'Could not determine verification status from code context',
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Check if the matched code is using a static/hardcoded value.
|
|
266
|
+
*/
|
|
267
|
+
_isStaticValue(line, matched) {
|
|
268
|
+
// If the finding line is a comment, it's static
|
|
269
|
+
if (/^\s*(?:\/\/|#|\*|\/\*)/.test(line)) return true;
|
|
270
|
+
|
|
271
|
+
// If the matched text is just a string literal with no interpolation
|
|
272
|
+
if (/^['"][^'"]*['"]$/.test(matched)) return true;
|
|
273
|
+
|
|
274
|
+
// If the line is a const assignment to a string literal
|
|
275
|
+
if (/const\s+\w+\s*=\s*['"][^'"]*['"]/.test(line)) return true;
|
|
276
|
+
|
|
277
|
+
// If it looks like a TODO/placeholder comment
|
|
278
|
+
if (/TODO|FIXME|EXAMPLE|PLACEHOLDER|SAMPLE/i.test(line)) return true;
|
|
279
|
+
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Check if a line is after a return/throw (dead code).
|
|
285
|
+
*/
|
|
286
|
+
_isDeadCode(lines, lineNum) {
|
|
287
|
+
// Check the 5 lines before the finding for return/throw
|
|
288
|
+
for (let i = Math.max(0, lineNum - 6); i < lineNum - 1; i++) {
|
|
289
|
+
const l = lines[i]?.trim() || '';
|
|
290
|
+
// If a return/throw is found and there's no conditional/block opener after
|
|
291
|
+
if (/^(?:return\s|throw\s|process\.exit)/.test(l)) {
|
|
292
|
+
// Check if there's a } or else between the return and our line
|
|
293
|
+
const between = lines.slice(i + 1, lineNum - 1).join('\n');
|
|
294
|
+
if (!/[{}]|else|case/.test(between)) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export default VerifierAgent;
|