thumbgate 1.3.0 → 1.4.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/.claude-plugin/marketplace.json +32 -13
- package/.claude-plugin/plugin.json +15 -2
- package/.well-known/llms.txt +60 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +109 -20
- package/adapters/README.md +1 -1
- package/adapters/chatgpt/openapi.yaml +168 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +84 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +200 -13
- package/bin/postinstall.js +8 -2
- package/config/budget.json +18 -0
- package/config/gates/code-edit.json +61 -0
- package/config/gates/db-write.json +61 -0
- package/config/gates/default.json +154 -3
- package/config/gates/deploy.json +61 -0
- package/config/github-about.json +2 -1
- package/config/merge-quality-checks.json +23 -0
- package/openapi/openapi.yaml +168 -0
- package/package.json +42 -10
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +27 -4
- package/plugins/codex-profile/README.md +33 -9
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/blog.html +73 -0
- package/public/compare/mem0.html +189 -0
- package/public/compare/speclock.html +180 -0
- package/public/compare.html +10 -2
- package/public/guide.html +2 -2
- package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
- package/public/guides/codex-cli-guardrails.html +158 -0
- package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
- package/public/guides/pre-action-gates.html +162 -0
- package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
- package/public/index.html +136 -50
- package/public/lessons.html +33 -24
- package/public/llm-context.md +140 -0
- package/public/pro.html +24 -22
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/access-anomaly-detector.js +1 -1
- package/scripts/adk-consolidator.js +1 -5
- package/scripts/agent-security-hardening.js +4 -6
- package/scripts/agentic-data-pipeline.js +1 -3
- package/scripts/async-job-runner.js +1 -5
- package/scripts/audit-trail.js +1 -5
- package/scripts/background-agent-governance.js +2 -10
- package/scripts/billing.js +2 -16
- package/scripts/budget-enforcer.js +173 -0
- package/scripts/build-codex-plugin.js +152 -0
- package/scripts/check-congruence.js +132 -14
- package/scripts/commercial-offer.js +5 -7
- package/scripts/content-engine/linkedin-content-generator.js +154 -0
- package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
- package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
- package/scripts/content-engine/reddit-thread-finder.js +154 -0
- package/scripts/context-engine.js +21 -6
- package/scripts/contextfs.js +1 -21
- package/scripts/dashboard.js +20 -0
- package/scripts/decision-journal.js +341 -0
- package/scripts/delegation-runtime.js +1 -5
- package/scripts/distribution-surfaces.js +26 -0
- package/scripts/document-intake.js +927 -0
- package/scripts/ephemeral-agent-store.js +1 -8
- package/scripts/evolution-state.js +1 -5
- package/scripts/experiment-tracker.js +1 -5
- package/scripts/export-databricks-bundle.js +1 -5
- package/scripts/export-hf-dataset.js +1 -5
- package/scripts/export-training.js +1 -5
- package/scripts/feedback-attribution.js +1 -16
- package/scripts/feedback-history-distiller.js +1 -16
- package/scripts/feedback-loop.js +1 -5
- package/scripts/feedback-root-consolidator.js +2 -21
- package/scripts/feedback-session.js +49 -0
- package/scripts/feedback-to-rules.js +188 -28
- package/scripts/filesystem-search.js +1 -9
- package/scripts/fs-utils.js +104 -0
- package/scripts/gates-engine.js +149 -4
- package/scripts/github-about.js +32 -8
- package/scripts/gtm-revenue-loop.js +1 -5
- package/scripts/harness-selector.js +148 -0
- package/scripts/hosted-job-launcher.js +1 -5
- package/scripts/hybrid-feedback-context.js +7 -33
- package/scripts/intervention-policy.js +58 -1
- package/scripts/lesson-db.js +3 -18
- package/scripts/lesson-inference.js +194 -16
- package/scripts/lesson-retrieval.js +60 -24
- package/scripts/llm-client.js +59 -0
- package/scripts/managed-lesson-agent.js +183 -0
- package/scripts/marketing-experiment.js +8 -22
- package/scripts/meta-agent-loop.js +624 -0
- package/scripts/metered-billing.js +1 -1
- package/scripts/money-watcher.js +1 -4
- package/scripts/obsidian-export.js +1 -5
- package/scripts/operational-integrity.js +15 -3
- package/scripts/org-dashboard.js +6 -1
- package/scripts/per-step-scoring.js +2 -4
- package/scripts/pr-manager.js +201 -19
- package/scripts/pro-features.js +3 -2
- package/scripts/prompt-dlp.js +3 -3
- package/scripts/prove-adapters.js +1 -5
- package/scripts/prove-attribution.js +1 -5
- package/scripts/prove-automation.js +1 -3
- package/scripts/prove-cloudflare-sandbox.js +1 -3
- package/scripts/prove-data-pipeline.js +1 -3
- package/scripts/prove-intelligence.js +1 -3
- package/scripts/prove-lancedb.js +1 -5
- package/scripts/prove-local-intelligence.js +1 -3
- package/scripts/prove-packaged-runtime.js +75 -9
- package/scripts/prove-predictive-insights.js +1 -3
- package/scripts/prove-training-export.js +1 -3
- package/scripts/prove-workflow-contract.js +1 -5
- package/scripts/rate-limiter.js +3 -1
- package/scripts/reddit-dm-outreach.js +14 -4
- package/scripts/schedule-manager.js +3 -5
- package/scripts/security-scanner.js +448 -0
- package/scripts/self-distill-agent.js +579 -0
- package/scripts/semantic-dedup.js +115 -0
- package/scripts/skill-exporter.js +1 -3
- package/scripts/skill-generator.js +1 -5
- package/scripts/social-analytics/engagement-audit.js +1 -18
- package/scripts/social-analytics/pollers/linkedin.js +26 -16
- package/scripts/social-analytics/publishers/linkedin.js +1 -1
- package/scripts/social-analytics/publishers/zernio.js +51 -0
- package/scripts/social-pipeline.js +1 -3
- package/scripts/social-post-hourly.js +47 -4
- package/scripts/statusline-links.js +6 -5
- package/scripts/statusline.sh +29 -153
- package/scripts/sync-branch-protection.js +340 -0
- package/scripts/tessl-export.js +1 -3
- package/scripts/thumbgate-search.js +32 -1
- package/scripts/tool-kpi-tracker.js +1 -1
- package/scripts/tool-registry.js +106 -2
- package/scripts/vector-store.js +1 -5
- package/scripts/weekly-auto-post.js +1 -1
- package/scripts/workflow-sentinel.js +91 -0
- package/skills/thumbgate/SKILL.md +1 -1
- package/src/api/server.js +273 -4
- package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
- /package/scripts/social-analytics/db/{social-analytics.db-wal → analytics.sqlite} +0 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Security Scanner — OWASP-aware static analysis for PreToolUse gates.
|
|
6
|
+
*
|
|
7
|
+
* Scans code being written/edited by AI agents for common vulnerability
|
|
8
|
+
* patterns (injection, XSS, path traversal, etc.) and suspicious dependency
|
|
9
|
+
* changes. Designed to run in the hot path of PreToolUse hooks with <50ms
|
|
10
|
+
* latency for pattern-match tier; deeper analysis is opt-in.
|
|
11
|
+
*
|
|
12
|
+
* Tier 1 (always): regex pattern matching — fast, zero external deps
|
|
13
|
+
* Tier 2 (high-risk): AST-level checks for dependency mutations
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { recordAuditEvent, auditToFeedback } = require('./audit-trail');
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Vulnerability pattern definitions (OWASP Top 10 + supply chain)
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
const VULN_PATTERNS = [
|
|
25
|
+
// Injection
|
|
26
|
+
{
|
|
27
|
+
id: 'cmd-injection',
|
|
28
|
+
category: 'injection',
|
|
29
|
+
severity: 'critical',
|
|
30
|
+
label: 'Command injection via unsanitized input',
|
|
31
|
+
regex: /\bexec(?:Sync)?\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+\s*(?:req\.|input|args|params|query|body|user))/g,
|
|
32
|
+
fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'shell-interpolation',
|
|
36
|
+
category: 'injection',
|
|
37
|
+
severity: 'critical',
|
|
38
|
+
label: 'Shell command with string interpolation',
|
|
39
|
+
regex: /\bexec(?:Sync)?\s*\(\s*`[^`]*\$\{[^}]*(?:req\.|input|args|params|query|body|user|process\.env)/g,
|
|
40
|
+
fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'sql-injection',
|
|
44
|
+
category: 'injection',
|
|
45
|
+
severity: 'critical',
|
|
46
|
+
label: 'Potential SQL injection via string concatenation',
|
|
47
|
+
regex: /(?:query|execute|run|all|get)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+\s*(?:req\.|input|args|params|query|body|user))/g,
|
|
48
|
+
fileTypes: ['.js', '.ts', '.mjs', '.cjs', '.py'],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'eval-usage',
|
|
52
|
+
category: 'injection',
|
|
53
|
+
severity: 'high',
|
|
54
|
+
label: 'Dynamic code execution (eval/Function constructor)',
|
|
55
|
+
regex: /\b(?:eval|new\s+Function)\s*\([^)]*(?:req\.|input|args|params|query|body|user)/g,
|
|
56
|
+
fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// XSS
|
|
60
|
+
{
|
|
61
|
+
id: 'xss-innerhtml',
|
|
62
|
+
category: 'xss',
|
|
63
|
+
severity: 'high',
|
|
64
|
+
label: 'Potential XSS via innerHTML assignment',
|
|
65
|
+
regex: /\.innerHTML\s*=\s*(?!['"]<(?:div|span|p|br|hr)\s*\/?>['"])/g,
|
|
66
|
+
fileTypes: ['.js', '.ts', '.jsx', '.tsx', '.mjs'],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'xss-dangerously-set',
|
|
70
|
+
category: 'xss',
|
|
71
|
+
severity: 'high',
|
|
72
|
+
label: 'React dangerouslySetInnerHTML with dynamic content',
|
|
73
|
+
regex: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*(?!['"])/g,
|
|
74
|
+
fileTypes: ['.jsx', '.tsx', '.js', '.ts'],
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// Path traversal
|
|
78
|
+
{
|
|
79
|
+
id: 'path-traversal',
|
|
80
|
+
category: 'path-traversal',
|
|
81
|
+
severity: 'critical',
|
|
82
|
+
label: 'Path traversal via unsanitized user input',
|
|
83
|
+
regex: /path\.(?:join|resolve)\s*\([^)]*(?:req\.|input|args|params|query|body|user)/g,
|
|
84
|
+
fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'path-traversal-direct',
|
|
88
|
+
category: 'path-traversal',
|
|
89
|
+
severity: 'high',
|
|
90
|
+
label: 'Direct file read with user-controlled path',
|
|
91
|
+
regex: /fs\.(?:readFile(?:Sync)?|createReadStream)\s*\(\s*(?:req\.|input|args|params|query|body|user)/g,
|
|
92
|
+
fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
// Prototype pollution
|
|
96
|
+
{
|
|
97
|
+
id: 'prototype-pollution',
|
|
98
|
+
category: 'prototype-pollution',
|
|
99
|
+
severity: 'high',
|
|
100
|
+
label: 'Potential prototype pollution via recursive merge',
|
|
101
|
+
regex: /(?:__proto__|constructor\s*\[\s*['"]prototype['"]\s*\]|Object\.assign\s*\(\s*\{\s*\})/g,
|
|
102
|
+
fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
// Insecure crypto
|
|
106
|
+
{
|
|
107
|
+
id: 'weak-hash',
|
|
108
|
+
category: 'crypto',
|
|
109
|
+
severity: 'medium',
|
|
110
|
+
label: 'Weak hash algorithm (MD5/SHA1) for security use',
|
|
111
|
+
regex: /createHash\s*\(\s*['"](?:md5|sha1)['"]\s*\)/gi,
|
|
112
|
+
fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'hardcoded-secret',
|
|
116
|
+
category: 'crypto',
|
|
117
|
+
severity: 'high',
|
|
118
|
+
label: 'Hardcoded secret/password in source code',
|
|
119
|
+
regex: /(?:password|secret|apiKey|api_key|token)\s*[:=]\s*['"][A-Za-z0-9+/=_-]{12,}['"]/g,
|
|
120
|
+
fileTypes: ['.js', '.ts', '.mjs', '.cjs', '.py', '.go', '.java'],
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// SSRF
|
|
124
|
+
{
|
|
125
|
+
id: 'ssrf-dynamic-url',
|
|
126
|
+
category: 'ssrf',
|
|
127
|
+
severity: 'high',
|
|
128
|
+
label: 'Potential SSRF via user-controlled URL',
|
|
129
|
+
regex: /(?:fetch|axios|got|request|https?\.(?:get|request))\s*\(\s*(?:`[^`]*\$\{|(?:req\.|input|args|params|query|body|user))/g,
|
|
130
|
+
fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
// Insecure deserialization
|
|
134
|
+
{
|
|
135
|
+
id: 'unsafe-deserialize',
|
|
136
|
+
category: 'deserialization',
|
|
137
|
+
severity: 'critical',
|
|
138
|
+
label: 'Unsafe deserialization of untrusted data',
|
|
139
|
+
regex: /(?:unserialize|yaml\.load\s*\((?!.*Loader\s*=\s*yaml\.SafeLoader)|pickle\.loads?|Marshal\.load)/g,
|
|
140
|
+
fileTypes: ['.js', '.ts', '.py', '.rb'],
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Supply chain patterns (dependency mutations)
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
const SUPPLY_CHAIN_PATTERNS = [
|
|
149
|
+
{
|
|
150
|
+
id: 'typosquat-suspect',
|
|
151
|
+
category: 'supply-chain',
|
|
152
|
+
severity: 'high',
|
|
153
|
+
label: 'Potentially typosquatted package name',
|
|
154
|
+
// Common typosquat indicators: single-char substitutions of popular packages
|
|
155
|
+
knownSafe: new Set([
|
|
156
|
+
'express', 'lodash', 'axios', 'react', 'vue', 'angular', 'moment',
|
|
157
|
+
'chalk', 'commander', 'inquirer', 'jest', 'mocha', 'webpack',
|
|
158
|
+
'typescript', 'eslint', 'prettier', 'nodemon', 'dotenv', 'cors',
|
|
159
|
+
'uuid', 'debug', 'semver', 'glob', 'minimatch', 'yargs',
|
|
160
|
+
]),
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'install-script-abuse',
|
|
164
|
+
category: 'supply-chain',
|
|
165
|
+
severity: 'critical',
|
|
166
|
+
label: 'Suspicious install script in package.json',
|
|
167
|
+
regex: /["'](?:pre|post)?install["']\s*:\s*["'](?:.*(?:curl|wget|nc\s|bash\s|sh\s|eval|exec|child_process))/g,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'dep-version-wildcard',
|
|
171
|
+
category: 'supply-chain',
|
|
172
|
+
severity: 'medium',
|
|
173
|
+
label: 'Wildcard or latest version in dependency',
|
|
174
|
+
regex: /["'](?:dependencies|devDependencies|peerDependencies)["'][\s\S]{0,500}?["'][^"']+["']\s*:\s*["'](?:\*|latest|>=)/g,
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Core scanning functions
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Scan code content for vulnerability patterns.
|
|
184
|
+
* @param {string} content - The code content to scan
|
|
185
|
+
* @param {string} filePath - The file path (for file-type filtering)
|
|
186
|
+
* @returns {{ detected: boolean, findings: Array<Object> }}
|
|
187
|
+
*/
|
|
188
|
+
function scanCode(content, filePath = '') {
|
|
189
|
+
if (!content || typeof content !== 'string') {
|
|
190
|
+
return { detected: false, findings: [] };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
194
|
+
const findings = [];
|
|
195
|
+
|
|
196
|
+
for (const pattern of VULN_PATTERNS) {
|
|
197
|
+
// Skip patterns that don't apply to this file type
|
|
198
|
+
if (pattern.fileTypes && pattern.fileTypes.length > 0 && ext && !pattern.fileTypes.includes(ext)) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Reset regex lastIndex for global patterns
|
|
203
|
+
pattern.regex.lastIndex = 0;
|
|
204
|
+
let match;
|
|
205
|
+
while ((match = pattern.regex.exec(content)) !== null) {
|
|
206
|
+
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
207
|
+
findings.push({
|
|
208
|
+
id: pattern.id,
|
|
209
|
+
category: pattern.category,
|
|
210
|
+
severity: pattern.severity,
|
|
211
|
+
label: pattern.label,
|
|
212
|
+
line: lineNumber,
|
|
213
|
+
match: match[0].slice(0, 120),
|
|
214
|
+
path: filePath,
|
|
215
|
+
});
|
|
216
|
+
// Only report first match per pattern per file to avoid noise
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
detected: findings.length > 0,
|
|
223
|
+
findings,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Scan dependency changes in package.json mutations.
|
|
229
|
+
* @param {string} oldContent - Previous package.json content (empty string if new file)
|
|
230
|
+
* @param {string} newContent - New package.json content
|
|
231
|
+
* @returns {{ detected: boolean, findings: Array<Object> }}
|
|
232
|
+
*/
|
|
233
|
+
function scanDependencyChange(oldContent, newContent) {
|
|
234
|
+
const findings = [];
|
|
235
|
+
|
|
236
|
+
if (!newContent) return { detected: false, findings: [] };
|
|
237
|
+
|
|
238
|
+
let newPkg;
|
|
239
|
+
try {
|
|
240
|
+
newPkg = JSON.parse(newContent);
|
|
241
|
+
} catch {
|
|
242
|
+
return { detected: false, findings: [] };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let oldPkg = {};
|
|
246
|
+
if (oldContent) {
|
|
247
|
+
try { oldPkg = JSON.parse(oldContent); } catch { /* treat as empty */ }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const depSections = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
251
|
+
|
|
252
|
+
// Check for new dependencies added
|
|
253
|
+
for (const section of depSections) {
|
|
254
|
+
const oldDeps = (oldPkg[section] || {});
|
|
255
|
+
const newDeps = (newPkg[section] || {});
|
|
256
|
+
|
|
257
|
+
for (const [pkg, version] of Object.entries(newDeps)) {
|
|
258
|
+
if (!oldDeps[pkg]) {
|
|
259
|
+
// New dependency added — check for red flags
|
|
260
|
+
if (version === '*' || version === 'latest' || version.startsWith('>=')) {
|
|
261
|
+
findings.push({
|
|
262
|
+
id: 'dep-version-wildcard',
|
|
263
|
+
category: 'supply-chain',
|
|
264
|
+
severity: 'medium',
|
|
265
|
+
label: `Wildcard version for new dependency: ${pkg}@${version}`,
|
|
266
|
+
path: 'package.json',
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Check for packages with suspicious names (very short, similar to popular ones)
|
|
271
|
+
if (pkg.length <= 2 && !['fs', 'os', 'vm'].includes(pkg)) {
|
|
272
|
+
findings.push({
|
|
273
|
+
id: 'suspicious-pkg-name',
|
|
274
|
+
category: 'supply-chain',
|
|
275
|
+
severity: 'high',
|
|
276
|
+
label: `Suspiciously short package name: "${pkg}"`,
|
|
277
|
+
path: 'package.json',
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Check for suspicious install scripts
|
|
285
|
+
const scripts = newPkg.scripts || {};
|
|
286
|
+
const dangerousScriptPatterns = /curl|wget|nc\s|bash\s-c|sh\s-c|eval\s|child_process|\.exec\(/i;
|
|
287
|
+
for (const [name, cmd] of Object.entries(scripts)) {
|
|
288
|
+
if (/^(?:pre|post)?install$/.test(name) && dangerousScriptPatterns.test(cmd)) {
|
|
289
|
+
findings.push({
|
|
290
|
+
id: 'install-script-abuse',
|
|
291
|
+
category: 'supply-chain',
|
|
292
|
+
severity: 'critical',
|
|
293
|
+
label: `Suspicious install script: ${name} → ${cmd.slice(0, 80)}`,
|
|
294
|
+
path: 'package.json',
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
detected: findings.length > 0,
|
|
301
|
+
findings,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// PreToolUse integration — called from gates-engine
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Evaluate security scan for a PreToolUse hook input.
|
|
311
|
+
* Returns a gate result if vulnerabilities are found, null otherwise.
|
|
312
|
+
*
|
|
313
|
+
* @param {Object} input - Hook input { tool_name, tool_input }
|
|
314
|
+
* @returns {Object|null} Gate result or null if clean
|
|
315
|
+
*/
|
|
316
|
+
function evaluateSecurityScan(input = {}) {
|
|
317
|
+
const toolName = input.tool_name || input.toolName || '';
|
|
318
|
+
const toolInput = input.tool_input || {};
|
|
319
|
+
|
|
320
|
+
// Only scan write-type operations
|
|
321
|
+
const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
322
|
+
if (!WRITE_TOOLS.has(toolName)) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const filePath = toolInput.file_path || toolInput.path || '';
|
|
327
|
+
const content = toolInput.content || toolInput.new_string || '';
|
|
328
|
+
|
|
329
|
+
if (!content) return null;
|
|
330
|
+
|
|
331
|
+
// Tier 1: Code vulnerability scan
|
|
332
|
+
const codeResult = scanCode(content, filePath);
|
|
333
|
+
|
|
334
|
+
// Tier 2: Supply chain scan for package.json changes
|
|
335
|
+
let supplyChainResult = { detected: false, findings: [] };
|
|
336
|
+
if (filePath && path.basename(filePath) === 'package.json') {
|
|
337
|
+
let oldContent = '';
|
|
338
|
+
try {
|
|
339
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
|
|
340
|
+
if (fs.existsSync(absPath)) {
|
|
341
|
+
oldContent = fs.readFileSync(absPath, 'utf8');
|
|
342
|
+
}
|
|
343
|
+
} catch { /* new file */ }
|
|
344
|
+
supplyChainResult = scanDependencyChange(oldContent, content);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const allFindings = [...codeResult.findings, ...supplyChainResult.findings];
|
|
348
|
+
if (allFindings.length === 0) return null;
|
|
349
|
+
|
|
350
|
+
// Determine overall severity
|
|
351
|
+
const hasCritical = allFindings.some(f => f.severity === 'critical');
|
|
352
|
+
const hasHigh = allFindings.some(f => f.severity === 'high');
|
|
353
|
+
const overallSeverity = hasCritical ? 'critical' : hasHigh ? 'high' : 'medium';
|
|
354
|
+
|
|
355
|
+
// Critical findings block; high/medium warn
|
|
356
|
+
const decision = hasCritical ? 'deny' : 'warn';
|
|
357
|
+
const gateId = 'security-vuln-scan';
|
|
358
|
+
const summary = allFindings.map(f =>
|
|
359
|
+
`[${f.severity.toUpperCase()}] ${f.label}${f.line ? ` (line ${f.line})` : ''}`
|
|
360
|
+
).join('; ');
|
|
361
|
+
|
|
362
|
+
const message = `Security scan detected ${allFindings.length} issue(s) in ${filePath || 'code'}: ${summary}`;
|
|
363
|
+
|
|
364
|
+
const reasoning = [
|
|
365
|
+
`Scanned ${content.length} bytes of content being written to ${filePath || 'unknown file'}`,
|
|
366
|
+
...allFindings.map(f => `${f.category}/${f.id}: ${f.label}${f.match ? ` — matched: ${f.match.slice(0, 60)}` : ''}`),
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
recordAuditEvent({
|
|
370
|
+
toolName,
|
|
371
|
+
toolInput: { file_path: filePath, content_length: content.length },
|
|
372
|
+
decision,
|
|
373
|
+
gateId,
|
|
374
|
+
message,
|
|
375
|
+
severity: overallSeverity,
|
|
376
|
+
source: 'security-scanner',
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
decision,
|
|
381
|
+
gate: gateId,
|
|
382
|
+
message,
|
|
383
|
+
severity: overallSeverity,
|
|
384
|
+
reasoning,
|
|
385
|
+
securityScan: {
|
|
386
|
+
findings: allFindings,
|
|
387
|
+
scannedBytes: content.length,
|
|
388
|
+
filePath,
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
// Self-heal integration — scan recent commits for vulnerabilities
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Scan git diff content for vulnerabilities introduced in recent changes.
|
|
399
|
+
* Intended for self-heal pipeline and post-commit auditing.
|
|
400
|
+
*
|
|
401
|
+
* @param {string} diffContent - Output of `git diff` or `git show`
|
|
402
|
+
* @returns {{ clean: boolean, findings: Array<Object> }}
|
|
403
|
+
*/
|
|
404
|
+
function scanGitDiff(diffContent) {
|
|
405
|
+
if (!diffContent) return { clean: true, findings: [] };
|
|
406
|
+
|
|
407
|
+
const allFindings = [];
|
|
408
|
+
let currentFile = '';
|
|
409
|
+
|
|
410
|
+
for (const line of diffContent.split('\n')) {
|
|
411
|
+
// Track current file from diff headers
|
|
412
|
+
const fileMatch = line.match(/^\+\+\+ b\/(.+)$/);
|
|
413
|
+
if (fileMatch) {
|
|
414
|
+
currentFile = fileMatch[1];
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Only scan added lines
|
|
419
|
+
if (!line.startsWith('+') || line.startsWith('+++')) continue;
|
|
420
|
+
|
|
421
|
+
const addedContent = line.slice(1);
|
|
422
|
+
const result = scanCode(addedContent, currentFile);
|
|
423
|
+
if (result.detected) {
|
|
424
|
+
for (const finding of result.findings) {
|
|
425
|
+
finding.path = currentFile;
|
|
426
|
+
allFindings.push(finding);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
clean: allFindings.length === 0,
|
|
433
|
+
findings: allFindings,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
// Exports
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
module.exports = {
|
|
442
|
+
VULN_PATTERNS,
|
|
443
|
+
SUPPLY_CHAIN_PATTERNS,
|
|
444
|
+
scanCode,
|
|
445
|
+
scanDependencyChange,
|
|
446
|
+
evaluateSecurityScan,
|
|
447
|
+
scanGitDiff,
|
|
448
|
+
};
|