agentshield-sdk 7.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/CHANGELOG.md +191 -0
- package/LICENSE +21 -0
- package/README.md +975 -0
- package/bin/agent-shield.js +680 -0
- package/package.json +118 -0
- package/src/adaptive.js +330 -0
- package/src/agent-protocol.js +998 -0
- package/src/alert-tuning.js +480 -0
- package/src/allowlist.js +603 -0
- package/src/audit-immutable.js +914 -0
- package/src/audit-streaming.js +469 -0
- package/src/badges.js +196 -0
- package/src/behavior-profiling.js +289 -0
- package/src/benchmark-harness.js +804 -0
- package/src/canary.js +271 -0
- package/src/certification.js +563 -0
- package/src/circuit-breaker.js +321 -0
- package/src/compliance.js +617 -0
- package/src/confidence-tuning.js +324 -0
- package/src/confused-deputy.js +624 -0
- package/src/context-scoring.js +360 -0
- package/src/conversation.js +494 -0
- package/src/cost-optimizer.js +1024 -0
- package/src/ctf.js +462 -0
- package/src/detector-core.js +1999 -0
- package/src/distributed.js +359 -0
- package/src/document-scanner.js +795 -0
- package/src/embedding.js +307 -0
- package/src/encoding.js +429 -0
- package/src/enterprise.js +405 -0
- package/src/errors.js +100 -0
- package/src/eu-ai-act.js +523 -0
- package/src/fuzzer.js +764 -0
- package/src/honeypot.js +328 -0
- package/src/i18n-patterns.js +523 -0
- package/src/index.js +430 -0
- package/src/integrations.js +528 -0
- package/src/llm-redteam.js +670 -0
- package/src/main.js +741 -0
- package/src/main.mjs +38 -0
- package/src/mcp-bridge.js +542 -0
- package/src/mcp-certification.js +846 -0
- package/src/mcp-sdk-integration.js +355 -0
- package/src/mcp-security-runtime.js +741 -0
- package/src/mcp-server.js +740 -0
- package/src/middleware.js +208 -0
- package/src/model-finetuning.js +884 -0
- package/src/model-fingerprint.js +1042 -0
- package/src/multi-agent-trust.js +453 -0
- package/src/multi-agent.js +404 -0
- package/src/multimodal.js +296 -0
- package/src/nist-mapping.js +505 -0
- package/src/observability.js +330 -0
- package/src/openclaw.js +450 -0
- package/src/otel.js +544 -0
- package/src/owasp-2025.js +483 -0
- package/src/pii.js +390 -0
- package/src/plugin-marketplace.js +628 -0
- package/src/plugin-system.js +349 -0
- package/src/policy-dsl.js +775 -0
- package/src/policy-extended.js +635 -0
- package/src/policy.js +443 -0
- package/src/presets.js +409 -0
- package/src/production.js +557 -0
- package/src/prompt-leakage.js +321 -0
- package/src/rag-vulnerability.js +579 -0
- package/src/redteam.js +475 -0
- package/src/response-handler.js +429 -0
- package/src/scanners.js +357 -0
- package/src/self-healing.js +363 -0
- package/src/semantic.js +339 -0
- package/src/shield-score.js +250 -0
- package/src/sso-saml.js +897 -0
- package/src/stream-scanner.js +806 -0
- package/src/testing.js +505 -0
- package/src/threat-encyclopedia.js +629 -0
- package/src/threat-intel-network.js +1017 -0
- package/src/token-analysis.js +467 -0
- package/src/tool-guard.js +412 -0
- package/src/tool-output-validator.js +354 -0
- package/src/utils.js +83 -0
- package/src/watermark.js +235 -0
- package/src/worker-scanner.js +601 -0
- package/types/index.d.ts +2088 -0
package/package.json
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentshield-sdk",
|
|
3
|
+
"version": "7.0.0",
|
|
4
|
+
"description": "The security standard for MCP and AI agents. Protects against prompt injection, confused deputy attacks, data exfiltration, and 30+ threats. Zero dependencies, runs locally.",
|
|
5
|
+
"main": "src/main.js",
|
|
6
|
+
"types": "types/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./src/main.mjs",
|
|
10
|
+
"require": "./src/main.js",
|
|
11
|
+
"types": "./types/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./core": "./src/index.js",
|
|
14
|
+
"./detector": "./src/detector-core.js",
|
|
15
|
+
"./middleware": "./src/middleware.js",
|
|
16
|
+
"./integrations": "./src/integrations.js",
|
|
17
|
+
"./mcp": "./src/mcp-sdk-integration.js",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"agent-shield": "bin/agent-shield.js"
|
|
22
|
+
},
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node test/test.js && node test/test-modules.js && node test/test-new-features.js",
|
|
26
|
+
"test:all": "node test/test-all-40-features.js",
|
|
27
|
+
"test:mcp": "node test/test-mcp-security.js",
|
|
28
|
+
"test:deputy": "node test/test-confused-deputy.js",
|
|
29
|
+
"test:v6": "node test/test-v6-modules.js",
|
|
30
|
+
"test:full": "npm test && node test/test-mcp-security.js && node test/test-confused-deputy.js && node test/test-v6-modules.js && npm run test:all",
|
|
31
|
+
"test:coverage": "c8 --reporter=text --reporter=lcov --reporter=json-summary npm test",
|
|
32
|
+
"lint": "node test/lint.js",
|
|
33
|
+
"lint:eslint": "eslint src/ test/ bin/",
|
|
34
|
+
"lint:eslint:fix": "eslint --fix src/ test/ bin/",
|
|
35
|
+
"format": "prettier --write \"src/**/*.js\" \"test/**/*.js\" \"bin/**/*.js\"",
|
|
36
|
+
"format:check": "prettier --check \"src/**/*.js\" \"test/**/*.js\" \"bin/**/*.js\"",
|
|
37
|
+
"score": "node -e \"const {ShieldScoreCalculator}=require('./src/shield-score');console.log(new ShieldScoreCalculator().formatReport())\"",
|
|
38
|
+
"benchmark": "node test/benchmark.js",
|
|
39
|
+
"redteam": "node -e \"const {AttackSimulator}=require('./src/redteam');const s=new AttackSimulator();s.runAll();console.log(s.formatReport())\"",
|
|
40
|
+
"test:fp": "node test/false-positives.js",
|
|
41
|
+
"test:adversarial": "node test/test-adversarial.js",
|
|
42
|
+
"audit": "npm audit --omit=dev",
|
|
43
|
+
"sbom": "node scripts/generate-sbom.js",
|
|
44
|
+
"mcp": "node src/mcp-server.js",
|
|
45
|
+
"sidecar": "node sidecar/server.js",
|
|
46
|
+
"ctf": "node -e \"const {CTFEngine,CTFReporter}=require('./src/ctf');const e=new CTFEngine();console.log(new CTFReporter().formatReport(e.getScoreboard()))\"",
|
|
47
|
+
"demo": "node bin/agent-shield.js demo",
|
|
48
|
+
"playground": "echo 'Open playground/index.html in a browser'",
|
|
49
|
+
"certify": "node -e \"const {CertificationRunner}=require('./src/certification');new CertificationRunner().runCertification().then(r=>console.log(r.certificate.toText()))\"",
|
|
50
|
+
"benchmark:run": "node scripts/run-benchmark.js",
|
|
51
|
+
"benchmark:generate": "node scripts/generate-dataset.js",
|
|
52
|
+
"benchmark:baseline": "node scripts/run-benchmark.js --save-baseline",
|
|
53
|
+
"benchmark:regression": "node scripts/run-benchmark.js --check-regression",
|
|
54
|
+
"prepublishOnly": "npm test && npm run test:all && npm run test:fp"
|
|
55
|
+
},
|
|
56
|
+
"keywords": [
|
|
57
|
+
"ai",
|
|
58
|
+
"security",
|
|
59
|
+
"prompt-injection",
|
|
60
|
+
"agent",
|
|
61
|
+
"llm",
|
|
62
|
+
"protection",
|
|
63
|
+
"shield",
|
|
64
|
+
"safety",
|
|
65
|
+
"firewall",
|
|
66
|
+
"pii",
|
|
67
|
+
"dlp",
|
|
68
|
+
"canary",
|
|
69
|
+
"watermark",
|
|
70
|
+
"red-team",
|
|
71
|
+
"compliance",
|
|
72
|
+
"enterprise",
|
|
73
|
+
"langchain",
|
|
74
|
+
"openai",
|
|
75
|
+
"anthropic",
|
|
76
|
+
"claude-sdk",
|
|
77
|
+
"sub-agent",
|
|
78
|
+
"mcp",
|
|
79
|
+
"model-context-protocol",
|
|
80
|
+
"confused-deputy",
|
|
81
|
+
"agent-trust",
|
|
82
|
+
"certification"
|
|
83
|
+
],
|
|
84
|
+
"author": "texasreaper62",
|
|
85
|
+
"license": "MIT",
|
|
86
|
+
"engines": {
|
|
87
|
+
"node": ">=16.0.0",
|
|
88
|
+
"npm": ">=8.0.0"
|
|
89
|
+
},
|
|
90
|
+
"publishConfig": {
|
|
91
|
+
"access": "public",
|
|
92
|
+
"registry": "https://registry.npmjs.org/"
|
|
93
|
+
},
|
|
94
|
+
"repository": {
|
|
95
|
+
"type": "git",
|
|
96
|
+
"url": "https://github.com/texasreaper62/Agent-Shield.git"
|
|
97
|
+
},
|
|
98
|
+
"bugs": {
|
|
99
|
+
"url": "https://github.com/texasreaper62/Agent-Shield/issues"
|
|
100
|
+
},
|
|
101
|
+
"homepage": "https://github.com/texasreaper62/Agent-Shield#readme",
|
|
102
|
+
"files": [
|
|
103
|
+
"src/**/*.js",
|
|
104
|
+
"src/**/*.mjs",
|
|
105
|
+
"types/index.d.ts",
|
|
106
|
+
"bin/agent-shield.js",
|
|
107
|
+
"LICENSE",
|
|
108
|
+
"README.md",
|
|
109
|
+
"CHANGELOG.md"
|
|
110
|
+
],
|
|
111
|
+
"devDependencies": {
|
|
112
|
+
"@eslint/js": "^10.0.1",
|
|
113
|
+
"c8": "^11.0.0",
|
|
114
|
+
"eslint": "^10.1.0",
|
|
115
|
+
"globals": "^17.4.0",
|
|
116
|
+
"prettier": "^3.4.0"
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/adaptive.js
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — Adaptive Detection, Semantic Hooks & Community Patterns
|
|
5
|
+
*
|
|
6
|
+
* - AdaptiveDetector: learns from false positives/negatives over time
|
|
7
|
+
* - SemanticAnalysisHook: pluggable LLM-based post-processing classifier
|
|
8
|
+
* - CommunityPatterns: load and merge detection patterns from local JSON
|
|
9
|
+
*
|
|
10
|
+
* All data stored locally — nothing is ever transmitted externally.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const crypto = require('crypto');
|
|
16
|
+
|
|
17
|
+
const LOG_PREFIX = '[Agent Shield]';
|
|
18
|
+
|
|
19
|
+
/** Extract character trigrams from a string. @param {string} text @returns {Set<string>} */
|
|
20
|
+
function trigrams(text) {
|
|
21
|
+
const t = text.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
22
|
+
const set = new Set();
|
|
23
|
+
for (let i = 0; i <= t.length - 3; i++) set.add(t.substring(i, i + 3));
|
|
24
|
+
return set;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Compute trigram similarity between two strings (0-1).
|
|
29
|
+
* @param {string} a
|
|
30
|
+
* @param {string} b
|
|
31
|
+
* @returns {number}
|
|
32
|
+
*/
|
|
33
|
+
function trigramSimilarity(a, b) {
|
|
34
|
+
const ta = trigrams(a);
|
|
35
|
+
const tb = trigrams(b);
|
|
36
|
+
if (ta.size === 0 || tb.size === 0) return 0;
|
|
37
|
+
let overlap = 0;
|
|
38
|
+
for (const t of ta) { if (tb.has(t)) overlap++; }
|
|
39
|
+
return overlap / Math.max(ta.size, tb.size);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** SHA-256 hash of a string (hex). @param {string} text @returns {string} */
|
|
43
|
+
function hash(text) {
|
|
44
|
+
return crypto.createHash('sha256').update(text).digest('hex');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Learns from false positives and false negatives to improve detection
|
|
49
|
+
* accuracy over time. Uses local file storage — no network calls.
|
|
50
|
+
*/
|
|
51
|
+
class AdaptiveDetector {
|
|
52
|
+
/**
|
|
53
|
+
* @param {object} [options]
|
|
54
|
+
* @param {string} [options.storePath] - Path to the JSON store file.
|
|
55
|
+
* @param {number} [options.similarityThreshold] - Trigram similarity threshold (0-1). Default 0.65.
|
|
56
|
+
*/
|
|
57
|
+
constructor(options = {}) {
|
|
58
|
+
this.storePath = options.storePath || path.join('.agent-shield', 'adaptive.json');
|
|
59
|
+
this.similarityThreshold = options.similarityThreshold || 0.65;
|
|
60
|
+
this.falsePositives = [];
|
|
61
|
+
this.falseNegatives = [];
|
|
62
|
+
this.stats = { suppressions: 0, boosts: 0, adjustments: 0 };
|
|
63
|
+
this.load();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Record a false positive so similar inputs can be suppressed in the future.
|
|
68
|
+
* @param {string} text - The input that was incorrectly flagged.
|
|
69
|
+
* @param {string} category - The threat category that was flagged.
|
|
70
|
+
*/
|
|
71
|
+
recordFalsePositive(text, category) {
|
|
72
|
+
const h = hash(text);
|
|
73
|
+
if (this.falsePositives.some(fp => fp.hash === h && fp.category === category)) return;
|
|
74
|
+
this.falsePositives.push({ hash: h, text, category, ts: Date.now() });
|
|
75
|
+
console.log(`${LOG_PREFIX} Recorded false positive for category "${category}"`);
|
|
76
|
+
this.save();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Record a false negative (missed attack) to boost future detection.
|
|
81
|
+
* @param {string} text - The input that should have been flagged.
|
|
82
|
+
* @param {string} category - The threat category that was missed.
|
|
83
|
+
*/
|
|
84
|
+
recordFalseNegative(text, category) {
|
|
85
|
+
const h = hash(text);
|
|
86
|
+
if (this.falseNegatives.some(fn => fn.hash === h && fn.category === category)) return;
|
|
87
|
+
this.falseNegatives.push({ hash: h, text, category, ts: Date.now() });
|
|
88
|
+
console.log(`${LOG_PREFIX} Recorded false negative for category "${category}"`);
|
|
89
|
+
this.save();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if text should be suppressed based on learned FP data.
|
|
94
|
+
* @param {string} text
|
|
95
|
+
* @param {string} category
|
|
96
|
+
* @returns {boolean}
|
|
97
|
+
*/
|
|
98
|
+
shouldSuppress(text, category) {
|
|
99
|
+
return this.falsePositives
|
|
100
|
+
.filter(fp => fp.category === category)
|
|
101
|
+
.some(fp => trigramSimilarity(text, fp.text) >= this.similarityThreshold);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get a confidence boost (0-20) if text resembles known false negatives.
|
|
106
|
+
* @param {string} text
|
|
107
|
+
* @param {string} category
|
|
108
|
+
* @returns {number}
|
|
109
|
+
*/
|
|
110
|
+
getBoost(text, category) {
|
|
111
|
+
let maxSim = 0;
|
|
112
|
+
for (const fn of this.falseNegatives) {
|
|
113
|
+
if (fn.category !== category) continue;
|
|
114
|
+
const sim = trigramSimilarity(text, fn.text);
|
|
115
|
+
if (sim > maxSim) maxSim = sim;
|
|
116
|
+
}
|
|
117
|
+
if (maxSim < this.similarityThreshold) return 0;
|
|
118
|
+
return Math.round(maxSim * 20);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Adjust a scan result based on learned data. Suppresses known FPs and
|
|
123
|
+
* boosts confidence for patterns similar to known FNs.
|
|
124
|
+
* @param {object} scanResult - A scan result object with threats array.
|
|
125
|
+
* @returns {object} Adjusted scan result.
|
|
126
|
+
*/
|
|
127
|
+
adjustResult(scanResult) {
|
|
128
|
+
if (!scanResult || !Array.isArray(scanResult.threats)) return scanResult;
|
|
129
|
+
const inputText = scanResult.input || '';
|
|
130
|
+
const adjusted = { ...scanResult, threats: [] };
|
|
131
|
+
for (const threat of scanResult.threats) {
|
|
132
|
+
const cat = threat.category || 'unknown';
|
|
133
|
+
if (this.shouldSuppress(inputText, cat)) {
|
|
134
|
+
this.stats.suppressions++;
|
|
135
|
+
this.stats.adjustments++;
|
|
136
|
+
console.log(`${LOG_PREFIX} Suppressing known false positive: ${cat}`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const boost = this.getBoost(inputText, cat);
|
|
140
|
+
if (boost > 0) {
|
|
141
|
+
this.stats.boosts++;
|
|
142
|
+
this.stats.adjustments++;
|
|
143
|
+
adjusted.threats.push({ ...threat, confidence: Math.min(100, (threat.confidence || 50) + boost) });
|
|
144
|
+
} else {
|
|
145
|
+
adjusted.threats.push(threat);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
adjusted.threatCount = adjusted.threats.length;
|
|
149
|
+
adjusted.blocked = adjusted.threats.some(t => t.severity === 'critical' || t.severity === 'high');
|
|
150
|
+
return adjusted;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Return learning statistics. @returns {object} */
|
|
154
|
+
getStats() {
|
|
155
|
+
return {
|
|
156
|
+
falsePositives: this.falsePositives.length,
|
|
157
|
+
falseNegatives: this.falseNegatives.length,
|
|
158
|
+
suppressions: this.stats.suppressions,
|
|
159
|
+
boosts: this.stats.boosts,
|
|
160
|
+
adjustments: this.stats.adjustments
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Persist learned data to disk. */
|
|
165
|
+
save() {
|
|
166
|
+
try {
|
|
167
|
+
const dir = path.dirname(this.storePath);
|
|
168
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
169
|
+
const data = JSON.stringify({
|
|
170
|
+
version: 1,
|
|
171
|
+
falsePositives: this.falsePositives,
|
|
172
|
+
falseNegatives: this.falseNegatives
|
|
173
|
+
}, null, 2);
|
|
174
|
+
fs.writeFileSync(this.storePath, data, 'utf8');
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.log(`${LOG_PREFIX} Failed to save adaptive data: ${err.message}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Load learned data from disk. */
|
|
181
|
+
load() {
|
|
182
|
+
try {
|
|
183
|
+
if (!fs.existsSync(this.storePath)) return;
|
|
184
|
+
const raw = fs.readFileSync(this.storePath, 'utf8');
|
|
185
|
+
const data = JSON.parse(raw);
|
|
186
|
+
this.falsePositives = Array.isArray(data.falsePositives) ? data.falsePositives : [];
|
|
187
|
+
this.falseNegatives = Array.isArray(data.falseNegatives) ? data.falseNegatives : [];
|
|
188
|
+
console.log(`${LOG_PREFIX} Loaded adaptive data: ${this.falsePositives.length} FPs, ${this.falseNegatives.length} FNs`);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.log(`${LOG_PREFIX} Failed to load adaptive data: ${err.message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Pluggable post-processing hook for user-supplied LLM classifiers.
|
|
197
|
+
* Falls back gracefully on errors or timeouts.
|
|
198
|
+
*/
|
|
199
|
+
class SemanticAnalysisHook {
|
|
200
|
+
/**
|
|
201
|
+
* @param {object} options
|
|
202
|
+
* @param {function} options.classifier - Async fn (text, threats) => { override: boolean, reason: string }
|
|
203
|
+
* @param {number} [options.timeoutMs=5000] - Classifier timeout in ms.
|
|
204
|
+
*/
|
|
205
|
+
constructor(options = {}) {
|
|
206
|
+
if (typeof options.classifier !== 'function') {
|
|
207
|
+
throw new Error('SemanticAnalysisHook requires a classifier function');
|
|
208
|
+
}
|
|
209
|
+
this.classifier = options.classifier;
|
|
210
|
+
this.timeoutMs = options.timeoutMs || 5000;
|
|
211
|
+
this.overrideCount = 0;
|
|
212
|
+
this.errorCount = 0;
|
|
213
|
+
this.totalLatency = 0;
|
|
214
|
+
this.callCount = 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Run the user's classifier and return an adjusted scan result.
|
|
219
|
+
* @param {string} text - The input text that was scanned.
|
|
220
|
+
* @param {object} scanResult - The original scan result.
|
|
221
|
+
* @returns {Promise<object>} Adjusted scan result.
|
|
222
|
+
*/
|
|
223
|
+
async analyze(text, scanResult) {
|
|
224
|
+
const start = Date.now();
|
|
225
|
+
try {
|
|
226
|
+
const threats = scanResult && Array.isArray(scanResult.threats) ? scanResult.threats : [];
|
|
227
|
+
const result = await Promise.race([
|
|
228
|
+
this.classifier(text, threats),
|
|
229
|
+
new Promise((_, reject) =>
|
|
230
|
+
setTimeout(() => reject(new Error('Classifier timed out')), this.timeoutMs)
|
|
231
|
+
)
|
|
232
|
+
]);
|
|
233
|
+
this.totalLatency += Date.now() - start;
|
|
234
|
+
this.callCount++;
|
|
235
|
+
if (result && result.override === true) {
|
|
236
|
+
this.overrideCount++;
|
|
237
|
+
console.log(`${LOG_PREFIX} Semantic hook override: ${result.reason || 'no reason given'}`);
|
|
238
|
+
return { ...scanResult, threats: [], threatCount: 0, semanticOverride: true, semanticReason: result.reason || '' };
|
|
239
|
+
}
|
|
240
|
+
return scanResult;
|
|
241
|
+
} catch (err) {
|
|
242
|
+
this.totalLatency += Date.now() - start;
|
|
243
|
+
this.callCount++;
|
|
244
|
+
this.errorCount++;
|
|
245
|
+
console.log(`${LOG_PREFIX} Semantic hook error: ${err.message}`);
|
|
246
|
+
return scanResult;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Return hook statistics. @returns {object} */
|
|
251
|
+
getStats() {
|
|
252
|
+
return {
|
|
253
|
+
overrideCount: this.overrideCount,
|
|
254
|
+
errorCount: this.errorCount,
|
|
255
|
+
callCount: this.callCount,
|
|
256
|
+
avgLatencyMs: this.callCount > 0 ? Math.round(this.totalLatency / this.callCount) : 0
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Loads and merges detection patterns from a local JSON file. The user is
|
|
263
|
+
* responsible for downloading/maintaining the file — no network calls.
|
|
264
|
+
*/
|
|
265
|
+
class CommunityPatterns {
|
|
266
|
+
/**
|
|
267
|
+
* @param {object} [options]
|
|
268
|
+
* @param {string} [options.path] - Path to the community patterns JSON file.
|
|
269
|
+
*/
|
|
270
|
+
constructor(options = {}) {
|
|
271
|
+
this.filePath = options.path || 'community-patterns.json';
|
|
272
|
+
this.patterns = [];
|
|
273
|
+
this.version = null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Read and parse the patterns file.
|
|
278
|
+
* @returns {boolean} True if loaded successfully.
|
|
279
|
+
*/
|
|
280
|
+
load() {
|
|
281
|
+
try {
|
|
282
|
+
const raw = fs.readFileSync(this.filePath, 'utf8');
|
|
283
|
+
const data = JSON.parse(raw);
|
|
284
|
+
this.version = data.version || null;
|
|
285
|
+
this.patterns = Array.isArray(data.patterns) ? data.patterns.map(p => ({
|
|
286
|
+
regex: p.regex || '',
|
|
287
|
+
severity: p.severity || 'medium',
|
|
288
|
+
category: p.category || 'community',
|
|
289
|
+
description: p.description || ''
|
|
290
|
+
})) : [];
|
|
291
|
+
console.log(`${LOG_PREFIX} Loaded ${this.patterns.length} community patterns (v${this.version})`);
|
|
292
|
+
return true;
|
|
293
|
+
} catch (err) {
|
|
294
|
+
console.log(`${LOG_PREFIX} Failed to load community patterns: ${err.message}`);
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Return the loaded patterns.
|
|
301
|
+
* @returns {Array<{regex: string, severity: string, category: string, description: string}>}
|
|
302
|
+
*/
|
|
303
|
+
getPatterns() {
|
|
304
|
+
return this.patterns;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Merge community patterns into an existing patterns array.
|
|
309
|
+
* @param {Array} existingPatterns - The current detection patterns.
|
|
310
|
+
* @returns {Array} Combined pattern array.
|
|
311
|
+
*/
|
|
312
|
+
merge(existingPatterns) {
|
|
313
|
+
const existing = Array.isArray(existingPatterns) ? existingPatterns : [];
|
|
314
|
+
const merged = [...existing];
|
|
315
|
+
for (const cp of this.patterns) {
|
|
316
|
+
if (!cp.regex) continue;
|
|
317
|
+
const alreadyExists = merged.some(p => (p.regex || p.pattern || '').toString() === cp.regex);
|
|
318
|
+
if (!alreadyExists) merged.push(cp);
|
|
319
|
+
}
|
|
320
|
+
console.log(`${LOG_PREFIX} Merged: ${existing.length} existing + ${merged.length - existing.length} community = ${merged.length} total`);
|
|
321
|
+
return merged;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Return the version string from the pattern file. @returns {string|null} */
|
|
325
|
+
getVersion() {
|
|
326
|
+
return this.version;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = { AdaptiveDetector, SemanticAnalysisHook, CommunityPatterns, trigramSimilarity };
|