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,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-Fix Rules
|
|
3
|
+
* ================
|
|
4
|
+
*
|
|
5
|
+
* Pure functions that transform a source line to fix a security issue.
|
|
6
|
+
* Each rule maps to a finding rule name from the agents.
|
|
7
|
+
*
|
|
8
|
+
* Used by `praxis remediate --all` to auto-fix agent-detected issues
|
|
9
|
+
* beyond just secrets.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const AUTOFIX_RULES = [
|
|
13
|
+
{
|
|
14
|
+
rule: 'TLS_REJECT_UNAUTHORIZED',
|
|
15
|
+
match: /rejectUnauthorized\s*:\s*false/g,
|
|
16
|
+
replace: (line) => line.replace(/rejectUnauthorized\s*:\s*false/, 'rejectUnauthorized: true // TODO: configure proper CA bundle'),
|
|
17
|
+
description: 'Enable TLS certificate verification',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
rule: 'DOCKER_LATEST_TAG',
|
|
21
|
+
match: /FROM\s+(\S+):latest/gi,
|
|
22
|
+
replace: (line) => {
|
|
23
|
+
return line.replace(/FROM\s+(\S+):latest/i, (_, image) => {
|
|
24
|
+
const pinned = { node: 'node:20-alpine', python: 'python:3.12-slim', nginx: 'nginx:1.25-alpine', ruby: 'ruby:3.3-slim' };
|
|
25
|
+
return `FROM ${pinned[image] || image + ':latest'} # TODO: pin to specific version`;
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
description: 'Pin Docker base image to a specific version',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
rule: 'DEBUG_MODE_PRODUCTION',
|
|
32
|
+
match: /(?:DEBUG|debug)\s*[:=]\s*(?:true|True|1|['"]true['"])/g,
|
|
33
|
+
replace: (line) => line
|
|
34
|
+
.replace(/DEBUG\s*=\s*True/, 'DEBUG = False')
|
|
35
|
+
.replace(/DEBUG\s*=\s*true/, 'DEBUG = false')
|
|
36
|
+
.replace(/debug\s*:\s*true/, 'debug: false')
|
|
37
|
+
.replace(/debug\s*=\s*['"]true['"]/, "debug = 'false'"),
|
|
38
|
+
description: 'Disable debug mode for production',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
rule: 'XSS_DANGEROUS_INNER_HTML',
|
|
42
|
+
match: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*([^}]+)\}\s*\}/g, // praxis-ignore: autofix pattern
|
|
43
|
+
replace: (line) => {
|
|
44
|
+
return line.replace(
|
|
45
|
+
/dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*([^}]+)\}\s*\}/,
|
|
46
|
+
(_, value) => `dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(${value.trim()}) }}` // praxis-ignore: replacement template
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
description: 'Wrap dangerouslySetInnerHTML value in DOMPurify.sanitize()',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
rule: 'CMD_INJECTION_SHELL_TRUE',
|
|
53
|
+
match: /shell\s*:\s*true/g,
|
|
54
|
+
replace: (line) => line.replace(/shell\s*:\s*true/, 'shell: false // TODO: ensure command works without shell'),
|
|
55
|
+
description: 'Disable shell execution in spawn/exec',
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if a finding's rule has an autofix available.
|
|
61
|
+
*/
|
|
62
|
+
export function hasAutofix(rule) {
|
|
63
|
+
return AUTOFIX_RULES.some(r => r.rule === rule);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Apply an autofix rule to a line.
|
|
68
|
+
* Returns the fixed line, or the original if no rule matches.
|
|
69
|
+
*/
|
|
70
|
+
export function applyAutofix(rule, line) {
|
|
71
|
+
const fixRule = AUTOFIX_RULES.find(r => r.rule === rule);
|
|
72
|
+
if (!fixRule) return line;
|
|
73
|
+
return fixRule.replace(line);
|
|
74
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Manager
|
|
3
|
+
* =============
|
|
4
|
+
*
|
|
5
|
+
* Provides incremental scanning by caching file hashes and findings.
|
|
6
|
+
* On subsequent runs, only changed files are re-scanned.
|
|
7
|
+
*
|
|
8
|
+
* Cache location: .praxis/context.json
|
|
9
|
+
*
|
|
10
|
+
* USAGE:
|
|
11
|
+
* import { CacheManager } from './cache-manager.js';
|
|
12
|
+
* const cache = new CacheManager(rootPath);
|
|
13
|
+
* const { changedFiles, cachedFindings } = await cache.getChangedFiles(currentFiles);
|
|
14
|
+
* // ... scan only changedFiles ...
|
|
15
|
+
* cache.save(allFiles, allFindings, recon, scoreResult);
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import crypto from 'crypto';
|
|
21
|
+
import { toolVersion } from '../core/version.js';
|
|
22
|
+
|
|
23
|
+
// The cache is keyed on the tool version, so a stale one would silently reuse stale
|
|
24
|
+
// findings. It comes from the single source in `cli/core/version.js`, which reports
|
|
25
|
+
// 'unknown' rather than guessing if package.json is unreadable — and a cache written
|
|
26
|
+
// under 'unknown' simply never matches, which is the safe direction.
|
|
27
|
+
const PACKAGE_VERSION = toolVersion();
|
|
28
|
+
|
|
29
|
+
// Cache TTL: 24 hours
|
|
30
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
export class CacheManager {
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} rootPath — Absolute path to project root
|
|
35
|
+
*/
|
|
36
|
+
constructor(rootPath) {
|
|
37
|
+
this.rootPath = rootPath;
|
|
38
|
+
this.cacheDir = path.join(rootPath, '.praxis');
|
|
39
|
+
this.cachePath = path.join(this.cacheDir, 'context.json');
|
|
40
|
+
this.cache = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load the cache from disk. Returns null if cache is missing, expired, or invalid.
|
|
45
|
+
*/
|
|
46
|
+
load() {
|
|
47
|
+
try {
|
|
48
|
+
if (!fs.existsSync(this.cachePath)) return null;
|
|
49
|
+
|
|
50
|
+
const raw = fs.readFileSync(this.cachePath, 'utf-8');
|
|
51
|
+
const cache = JSON.parse(raw);
|
|
52
|
+
|
|
53
|
+
// Version mismatch — patterns may have changed
|
|
54
|
+
if (cache.version !== PACKAGE_VERSION) return null;
|
|
55
|
+
|
|
56
|
+
// TTL expired
|
|
57
|
+
const age = Date.now() - new Date(cache.generatedAt).getTime();
|
|
58
|
+
if (age > CACHE_TTL_MS) return null;
|
|
59
|
+
|
|
60
|
+
this.cache = cache;
|
|
61
|
+
return cache;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Compute SHA-256 hash of a file's contents.
|
|
69
|
+
*/
|
|
70
|
+
hashFile(filePath) {
|
|
71
|
+
try {
|
|
72
|
+
const content = fs.readFileSync(filePath);
|
|
73
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Compare current files against cached file index to find what changed.
|
|
81
|
+
*
|
|
82
|
+
* @param {string[]} currentFiles — Array of absolute file paths
|
|
83
|
+
* @returns {{ changedFiles: string[], cachedFindings: object[], unchangedCount: number, newCount: number, modifiedCount: number, deletedCount: number }}
|
|
84
|
+
*/
|
|
85
|
+
diff(currentFiles) {
|
|
86
|
+
if (!this.cache || !this.cache.fileIndex) {
|
|
87
|
+
return {
|
|
88
|
+
changedFiles: currentFiles,
|
|
89
|
+
cachedFindings: [],
|
|
90
|
+
unchangedCount: 0,
|
|
91
|
+
newCount: currentFiles.length,
|
|
92
|
+
modifiedCount: 0,
|
|
93
|
+
deletedCount: 0,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const cachedIndex = this.cache.fileIndex;
|
|
98
|
+
const cachedFindings = this.cache.lastFindings || {};
|
|
99
|
+
const changedFiles = [];
|
|
100
|
+
const reusedFindings = [];
|
|
101
|
+
let unchangedCount = 0;
|
|
102
|
+
let newCount = 0;
|
|
103
|
+
let modifiedCount = 0;
|
|
104
|
+
|
|
105
|
+
const currentSet = new Set(currentFiles);
|
|
106
|
+
|
|
107
|
+
for (const file of currentFiles) {
|
|
108
|
+
const relPath = path.relative(this.rootPath, file).replace(/\\/g, '/');
|
|
109
|
+
const cached = cachedIndex[relPath];
|
|
110
|
+
|
|
111
|
+
if (!cached) {
|
|
112
|
+
// New file — needs scanning
|
|
113
|
+
changedFiles.push(file);
|
|
114
|
+
newCount++;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Quick size check before expensive hash
|
|
119
|
+
try {
|
|
120
|
+
const stats = fs.statSync(file);
|
|
121
|
+
if (stats.size !== cached.size) {
|
|
122
|
+
changedFiles.push(file);
|
|
123
|
+
modifiedCount++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
changedFiles.push(file);
|
|
128
|
+
modifiedCount++;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Hash check
|
|
133
|
+
const currentHash = this.hashFile(file);
|
|
134
|
+
if (currentHash !== cached.hash) {
|
|
135
|
+
changedFiles.push(file);
|
|
136
|
+
modifiedCount++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// File unchanged — reuse cached findings
|
|
141
|
+
unchangedCount++;
|
|
142
|
+
if (cachedFindings[relPath]) {
|
|
143
|
+
// Restore absolute paths for cached findings
|
|
144
|
+
for (const finding of cachedFindings[relPath]) {
|
|
145
|
+
reusedFindings.push({ ...finding, file });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Count deleted files (in cache but not in current)
|
|
151
|
+
const currentRelPaths = new Set(
|
|
152
|
+
currentFiles.map(f => path.relative(this.rootPath, f).replace(/\\/g, '/'))
|
|
153
|
+
);
|
|
154
|
+
const deletedCount = Object.keys(cachedIndex).filter(p => !currentRelPaths.has(p)).length;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
changedFiles,
|
|
158
|
+
cachedFindings: reusedFindings,
|
|
159
|
+
unchangedCount,
|
|
160
|
+
newCount,
|
|
161
|
+
modifiedCount,
|
|
162
|
+
deletedCount,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Save the cache to disk.
|
|
168
|
+
*
|
|
169
|
+
* @param {string[]} allFiles — All scanned file paths
|
|
170
|
+
* @param {object[]} allFindings — All findings from the scan
|
|
171
|
+
* @param {object} recon — ReconAgent output
|
|
172
|
+
* @param {object} [scoreResult] — Optional score result
|
|
173
|
+
*/
|
|
174
|
+
save(allFiles, allFindings, recon, scoreResult) {
|
|
175
|
+
try {
|
|
176
|
+
// Ensure .praxis directory exists
|
|
177
|
+
if (!fs.existsSync(this.cacheDir)) {
|
|
178
|
+
fs.mkdirSync(this.cacheDir, { recursive: true });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Build file index with hashes
|
|
182
|
+
const fileIndex = {};
|
|
183
|
+
for (const file of allFiles) {
|
|
184
|
+
const relPath = path.relative(this.rootPath, file).replace(/\\/g, '/');
|
|
185
|
+
const hash = this.hashFile(file);
|
|
186
|
+
if (hash) {
|
|
187
|
+
try {
|
|
188
|
+
const stats = fs.statSync(file);
|
|
189
|
+
fileIndex[relPath] = {
|
|
190
|
+
hash,
|
|
191
|
+
size: stats.size,
|
|
192
|
+
lastScanned: new Date().toISOString(),
|
|
193
|
+
};
|
|
194
|
+
} catch {
|
|
195
|
+
// Skip files we can't stat
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Group findings by file (relative paths)
|
|
201
|
+
const lastFindings = {};
|
|
202
|
+
for (const f of allFindings) {
|
|
203
|
+
const relPath = path.relative(this.rootPath, f.file).replace(/\\/g, '/');
|
|
204
|
+
if (!lastFindings[relPath]) lastFindings[relPath] = [];
|
|
205
|
+
// Store a lightweight copy (no absolute paths)
|
|
206
|
+
lastFindings[relPath].push({
|
|
207
|
+
line: f.line,
|
|
208
|
+
column: f.column,
|
|
209
|
+
severity: f.severity,
|
|
210
|
+
category: f.category,
|
|
211
|
+
rule: f.rule,
|
|
212
|
+
title: f.title,
|
|
213
|
+
description: f.description,
|
|
214
|
+
matched: f.matched,
|
|
215
|
+
confidence: f.confidence,
|
|
216
|
+
cwe: f.cwe,
|
|
217
|
+
owasp: f.owasp,
|
|
218
|
+
fix: f.fix,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const cache = {
|
|
223
|
+
version: PACKAGE_VERSION,
|
|
224
|
+
generatedAt: new Date().toISOString(),
|
|
225
|
+
rootPath: this.rootPath,
|
|
226
|
+
recon: recon || null,
|
|
227
|
+
fileIndex,
|
|
228
|
+
lastFindings,
|
|
229
|
+
stats: {
|
|
230
|
+
totalFiles: allFiles.length,
|
|
231
|
+
totalFindings: allFindings.length,
|
|
232
|
+
lastScore: scoreResult?.score ?? null,
|
|
233
|
+
lastGrade: scoreResult?.grade?.letter ?? null,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
fs.writeFileSync(this.cachePath, JSON.stringify(cache, null, 2));
|
|
238
|
+
} catch {
|
|
239
|
+
// Silent failure — caching should never break a scan
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Delete the cache file.
|
|
245
|
+
*/
|
|
246
|
+
invalidate() {
|
|
247
|
+
try {
|
|
248
|
+
if (fs.existsSync(this.cachePath)) {
|
|
249
|
+
fs.unlinkSync(this.cachePath);
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
// Silent
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ===========================================================================
|
|
257
|
+
// LLM CLASSIFICATION CACHE
|
|
258
|
+
// ===========================================================================
|
|
259
|
+
|
|
260
|
+
get llmCachePath() {
|
|
261
|
+
return path.join(this.cacheDir, 'llm-cache.json');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Generate a cache key for an LLM classification.
|
|
266
|
+
*/
|
|
267
|
+
getLLMCacheKey(finding) {
|
|
268
|
+
const data = `${finding.file}:${finding.line}:${finding.rule}:${finding.matched || ''}`;
|
|
269
|
+
return crypto.createHash('sha256').update(data).digest('hex').slice(0, 16);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Load cached LLM classifications. Returns {} if none or expired.
|
|
274
|
+
*/
|
|
275
|
+
loadLLMClassifications() {
|
|
276
|
+
const LLM_CACHE_TTL = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
277
|
+
try {
|
|
278
|
+
if (!fs.existsSync(this.llmCachePath)) return {};
|
|
279
|
+
const raw = JSON.parse(fs.readFileSync(this.llmCachePath, 'utf-8'));
|
|
280
|
+
const now = Date.now();
|
|
281
|
+
const valid = {};
|
|
282
|
+
for (const [key, entry] of Object.entries(raw)) {
|
|
283
|
+
if (now - new Date(entry.cachedAt).getTime() < LLM_CACHE_TTL) {
|
|
284
|
+
valid[key] = entry;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return valid;
|
|
288
|
+
} catch {
|
|
289
|
+
return {};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Save LLM classifications to cache.
|
|
295
|
+
*/
|
|
296
|
+
saveLLMClassifications(classifications) {
|
|
297
|
+
try {
|
|
298
|
+
if (!fs.existsSync(this.cacheDir)) {
|
|
299
|
+
fs.mkdirSync(this.cacheDir, { recursive: true });
|
|
300
|
+
}
|
|
301
|
+
const existing = this.loadLLMClassifications();
|
|
302
|
+
const merged = { ...existing, ...classifications };
|
|
303
|
+
fs.writeFileSync(this.llmCachePath, JSON.stringify(merged, null, 2));
|
|
304
|
+
} catch {
|
|
305
|
+
// Silent
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export default CacheManager;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compliance Mapping Utility
|
|
3
|
+
* ===========================
|
|
4
|
+
*
|
|
5
|
+
* Maps CWE/OWASP findings to compliance frameworks:
|
|
6
|
+
* - SOC 2 Type II (Trust Service Criteria)
|
|
7
|
+
* - ISO 27001:2022 (Annex A controls)
|
|
8
|
+
* - NIST AI Risk Management Framework (AI RMF 1.0)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// CWE → COMPLIANCE CONTROL MAPPING
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
const CWE_MAP = {
|
|
16
|
+
'CWE-74': { soc2: ['CC6.1', 'CC7.1'], iso27001: ['A.8.28'], nistAiRmf: ['MAP 1.5', 'MEASURE 2.6'] },
|
|
17
|
+
'CWE-78': { soc2: ['CC6.1', 'CC7.1'], iso27001: ['A.8.28'], nistAiRmf: ['MAP 1.5'] },
|
|
18
|
+
'CWE-79': { soc2: ['CC6.1'], iso27001: ['A.8.28'], nistAiRmf: [] },
|
|
19
|
+
'CWE-89': { soc2: ['CC6.1'], iso27001: ['A.8.28'], nistAiRmf: [] },
|
|
20
|
+
'CWE-94': { soc2: ['CC6.1', 'CC7.1'], iso27001: ['A.8.28', 'A.8.9'], nistAiRmf: ['MAP 1.5'] },
|
|
21
|
+
'CWE-116': { soc2: ['CC6.1'], iso27001: ['A.8.28'], nistAiRmf: ['MEASURE 2.6'] },
|
|
22
|
+
'CWE-200': { soc2: ['CC6.5', 'CC6.1'], iso27001: ['A.8.11', 'A.5.33'], nistAiRmf: ['GOVERN 1.7', 'MAP 5.1'] },
|
|
23
|
+
'CWE-250': { soc2: ['CC6.3'], iso27001: ['A.8.2'], nistAiRmf: ['GOVERN 1.4'] },
|
|
24
|
+
'CWE-269': { soc2: ['CC6.3', 'CC6.1'], iso27001: ['A.8.2', 'A.5.15'], nistAiRmf: ['GOVERN 1.4'] },
|
|
25
|
+
'CWE-287': { soc2: ['CC6.1', 'CC6.2'], iso27001: ['A.8.5'], nistAiRmf: [] },
|
|
26
|
+
'CWE-306': { soc2: ['CC6.1', 'CC6.2'], iso27001: ['A.8.5'], nistAiRmf: ['GOVERN 1.4'] },
|
|
27
|
+
'CWE-311': { soc2: ['CC6.7'], iso27001: ['A.8.24'], nistAiRmf: [] },
|
|
28
|
+
'CWE-312': { soc2: ['CC6.7', 'CC6.1'], iso27001: ['A.8.24', 'A.5.33'], nistAiRmf: ['MAP 5.1'] },
|
|
29
|
+
'CWE-326': { soc2: ['CC6.7'], iso27001: ['A.8.24'], nistAiRmf: [] },
|
|
30
|
+
'CWE-327': { soc2: ['CC6.7'], iso27001: ['A.8.24'], nistAiRmf: [] },
|
|
31
|
+
'CWE-502': { soc2: ['CC6.1'], iso27001: ['A.8.28'], nistAiRmf: [] },
|
|
32
|
+
'CWE-522': { soc2: ['CC6.1', 'CC6.7'], iso27001: ['A.8.5', 'A.8.24'], nistAiRmf: [] },
|
|
33
|
+
'CWE-611': { soc2: ['CC6.1'], iso27001: ['A.8.28'], nistAiRmf: [] },
|
|
34
|
+
'CWE-668': { soc2: ['CC6.1', 'CC6.6'], iso27001: ['A.8.9', 'A.8.20'], nistAiRmf: ['GOVERN 1.4'] },
|
|
35
|
+
'CWE-798': { soc2: ['CC6.1', 'CC6.7'], iso27001: ['A.5.33', 'A.8.24'], nistAiRmf: [] },
|
|
36
|
+
'CWE-918': { soc2: ['CC6.1', 'CC6.6'], iso27001: ['A.8.20', 'A.8.28'], nistAiRmf: [] },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// OWASP Agentic → NIST AI RMF mapping (agent-specific)
|
|
40
|
+
const AGENTIC_MAP = {
|
|
41
|
+
'ASI01': { soc2: ['CC6.1', 'CC7.2'], iso27001: ['A.8.28', 'A.8.9'], nistAiRmf: ['MAP 1.5', 'MEASURE 2.6', 'MANAGE 2.2'] },
|
|
42
|
+
'ASI02': { soc2: ['CC6.1', 'CC6.3'], iso27001: ['A.8.2', 'A.8.9'], nistAiRmf: ['MAP 1.5', 'GOVERN 1.4', 'MANAGE 2.2'] },
|
|
43
|
+
'ASI03': { soc2: ['CC6.3'], iso27001: ['A.8.2', 'A.5.15'], nistAiRmf: ['GOVERN 1.4', 'MAP 3.4'] },
|
|
44
|
+
'ASI04': { soc2: ['CC6.6', 'CC7.1'], iso27001: ['A.5.19', 'A.5.21'], nistAiRmf: ['MAP 1.5', 'GOVERN 6.1'] },
|
|
45
|
+
'ASI05': { soc2: ['CC6.1', 'CC7.1'], iso27001: ['A.8.28', 'A.8.9'], nistAiRmf: ['MAP 1.5'] },
|
|
46
|
+
'ASI06': { soc2: ['CC6.1'], iso27001: ['A.8.11'], nistAiRmf: ['MEASURE 2.6', 'MANAGE 2.2'] },
|
|
47
|
+
'ASI07': { soc2: ['CC6.1', 'CC6.7'], iso27001: ['A.8.20', 'A.8.24'], nistAiRmf: ['MAP 1.5'] },
|
|
48
|
+
'ASI08': { soc2: ['CC7.4', 'CC7.5'], iso27001: ['A.5.30'], nistAiRmf: ['MANAGE 4.1'] },
|
|
49
|
+
'ASI09': { soc2: ['CC6.2'], iso27001: ['A.8.5', 'A.5.15'], nistAiRmf: ['GOVERN 1.7'] },
|
|
50
|
+
'ASI10': { soc2: ['CC7.2', 'CC7.4'], iso27001: ['A.8.9', 'A.5.30'], nistAiRmf: ['MANAGE 2.2', 'MANAGE 4.1'] },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// OWASP AGENTIC AI TOP 10 (December 2025)
|
|
55
|
+
// =============================================================================
|
|
56
|
+
|
|
57
|
+
const OWASP_AGENTIC_TOP_10 = {
|
|
58
|
+
ASI01: { id: 'ASI01', title: 'Agent Goal Hijacking', description: 'Manipulation of agent objectives through prompt injection, memory poisoning, or instruction override.' },
|
|
59
|
+
ASI02: { id: 'ASI02', title: 'Tool Misuse', description: 'Agent uses tools in unintended or dangerous ways — shell execution, file deletion, network access beyond scope.' },
|
|
60
|
+
ASI03: { id: 'ASI03', title: 'Privilege Abuse', description: 'Agent operates with excessive permissions — writes outside project, accesses secrets, escalates access.' },
|
|
61
|
+
ASI04: { id: 'ASI04', title: 'Agentic Supply Chain', description: 'Compromised skills, MCP servers, or tool packages that the agent depends on.' },
|
|
62
|
+
ASI05: { id: 'ASI05', title: 'Memory & Context Poisoning', description: 'Malicious data persisted in agent memory, rules files, or context that survives sessions.' },
|
|
63
|
+
ASI06: { id: 'ASI06', title: 'Uncontrolled Data Exposure', description: 'Agent leaks code, secrets, or PII through tool outputs, logs, or external API calls.' },
|
|
64
|
+
ASI07: { id: 'ASI07', title: 'Insecure Communication', description: 'Unencrypted MCP transport, HTTP model endpoints, or plaintext inter-agent messaging.' },
|
|
65
|
+
ASI08: { id: 'ASI08', title: 'Missing Human Oversight', description: 'Agent takes destructive or irreversible actions without user confirmation — proactive mode risks.' },
|
|
66
|
+
ASI09: { id: 'ASI09', title: 'Weak Identity & Auth', description: 'Agent sessions without authentication, shared API keys, or no audit trail of actions.' },
|
|
67
|
+
ASI10: { id: 'ASI10', title: 'Rogue Agent Behavior', description: 'Agent deviates from intended behavior — self-modification, stealth mode, output suppression.' },
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Enrich a finding with OWASP Agentic Top 10 metadata.
|
|
72
|
+
* Attaches `agenticRisk` object if the finding maps to ASI01–ASI10.
|
|
73
|
+
* @param {object} finding
|
|
74
|
+
* @returns {object} — finding with agenticRisk attached (or unchanged)
|
|
75
|
+
*/
|
|
76
|
+
export function enrichAgenticRisk(finding) {
|
|
77
|
+
const owasp = finding.owasp;
|
|
78
|
+
if (!owasp || !OWASP_AGENTIC_TOP_10[owasp]) return finding;
|
|
79
|
+
|
|
80
|
+
const risk = OWASP_AGENTIC_TOP_10[owasp];
|
|
81
|
+
finding.agenticRisk = {
|
|
82
|
+
id: risk.id,
|
|
83
|
+
title: risk.title,
|
|
84
|
+
description: risk.description,
|
|
85
|
+
};
|
|
86
|
+
return finding;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get OWASP Agentic Top 10 summary across all findings.
|
|
91
|
+
* @param {object[]} findings
|
|
92
|
+
* @returns {{ risks: object[], coverage: number }}
|
|
93
|
+
*/
|
|
94
|
+
export function getAgenticSummary(findings) {
|
|
95
|
+
const counts = {};
|
|
96
|
+
for (const f of findings) {
|
|
97
|
+
const owasp = f.owasp;
|
|
98
|
+
if (owasp && OWASP_AGENTIC_TOP_10[owasp]) {
|
|
99
|
+
counts[owasp] = (counts[owasp] || 0) + 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const risks = Object.entries(OWASP_AGENTIC_TOP_10).map(([id, info]) => ({
|
|
104
|
+
...info,
|
|
105
|
+
findingCount: counts[id] || 0,
|
|
106
|
+
status: counts[id] ? 'flagged' : 'clear',
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
const flagged = risks.filter(r => r.findingCount > 0).length;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
risks,
|
|
113
|
+
flagged,
|
|
114
|
+
total: 10,
|
|
115
|
+
coverage: `${flagged}/10`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// =============================================================================
|
|
120
|
+
// PUBLIC API
|
|
121
|
+
// =============================================================================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Map a single finding to compliance controls.
|
|
125
|
+
* @param {object} finding - A finding with `cwe` and `owasp` fields.
|
|
126
|
+
* @returns {{ soc2: string[], iso27001: string[], nistAiRmf: string[] }}
|
|
127
|
+
*/
|
|
128
|
+
export function mapFindingToCompliance(finding) {
|
|
129
|
+
const result = { soc2: new Set(), iso27001: new Set(), nistAiRmf: new Set() };
|
|
130
|
+
|
|
131
|
+
// Map from CWE
|
|
132
|
+
const cwe = finding.cwe || finding.CWE;
|
|
133
|
+
if (cwe && CWE_MAP[cwe]) {
|
|
134
|
+
const m = CWE_MAP[cwe];
|
|
135
|
+
m.soc2.forEach(c => result.soc2.add(c));
|
|
136
|
+
m.iso27001.forEach(c => result.iso27001.add(c));
|
|
137
|
+
m.nistAiRmf.forEach(c => result.nistAiRmf.add(c));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Map from OWASP Agentic
|
|
141
|
+
const owasp = finding.owasp || finding.OWASP;
|
|
142
|
+
if (owasp && AGENTIC_MAP[owasp]) {
|
|
143
|
+
const m = AGENTIC_MAP[owasp];
|
|
144
|
+
m.soc2.forEach(c => result.soc2.add(c));
|
|
145
|
+
m.iso27001.forEach(c => result.iso27001.add(c));
|
|
146
|
+
m.nistAiRmf.forEach(c => result.nistAiRmf.add(c));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
soc2: [...result.soc2].sort(),
|
|
151
|
+
iso27001: [...result.iso27001].sort(),
|
|
152
|
+
nistAiRmf: [...result.nistAiRmf].sort(),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Aggregate compliance mappings across all findings.
|
|
158
|
+
* @param {object[]} findings - Array of findings.
|
|
159
|
+
* @returns {{ soc2: object, iso27001: object, nistAiRmf: object, summary: object }}
|
|
160
|
+
*/
|
|
161
|
+
export function getComplianceSummary(findings) {
|
|
162
|
+
const soc2 = {};
|
|
163
|
+
const iso27001 = {};
|
|
164
|
+
const nistAiRmf = {};
|
|
165
|
+
|
|
166
|
+
for (const f of findings) {
|
|
167
|
+
const mapped = mapFindingToCompliance(f);
|
|
168
|
+
|
|
169
|
+
for (const ctrl of mapped.soc2) {
|
|
170
|
+
soc2[ctrl] = (soc2[ctrl] || 0) + 1;
|
|
171
|
+
}
|
|
172
|
+
for (const ctrl of mapped.iso27001) {
|
|
173
|
+
iso27001[ctrl] = (iso27001[ctrl] || 0) + 1;
|
|
174
|
+
}
|
|
175
|
+
for (const ctrl of mapped.nistAiRmf) {
|
|
176
|
+
nistAiRmf[ctrl] = (nistAiRmf[ctrl] || 0) + 1;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
soc2,
|
|
182
|
+
iso27001,
|
|
183
|
+
nistAiRmf,
|
|
184
|
+
summary: {
|
|
185
|
+
soc2Controls: Object.keys(soc2).length,
|
|
186
|
+
iso27001Controls: Object.keys(iso27001).length,
|
|
187
|
+
nistAiRmfControls: Object.keys(nistAiRmf).length,
|
|
188
|
+
totalFindings: findings.length,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|