thumbgate 1.27.18 → 1.27.19
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 +6 -6
- package/.claude-plugin/plugin.json +4 -3
- package/.well-known/agentic-verify.txt +1 -0
- package/.well-known/llms.txt +33 -12
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +249 -30
- package/adapters/chatgpt/openapi.yaml +12 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/gemini/function-declarations.json +1 -0
- package/adapters/mcp/server-stdio.js +263 -11
- package/adapters/opencode/opencode.json +1 -1
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +1429 -121
- package/bin/postinstall.js +1 -8
- package/config/gate-classifier-routing.json +98 -0
- package/config/gate-templates.json +216 -0
- package/config/gates/claim-verification.json +12 -0
- package/config/gates/default.json +31 -2
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +23 -13
- package/config/merge-quality-checks.json +0 -1
- package/config/model-candidates.json +121 -6
- package/config/post-deploy-marketing-pages.json +80 -0
- package/config/tessl-tiles.json +1 -3
- package/openapi/openapi.yaml +12 -0
- package/package.json +1 -1
- package/public/blog.html +4 -4
- package/public/codex-plugin.html +72 -20
- package/public/compare.html +31 -8
- package/public/dashboard.html +930 -166
- package/public/federal.html +2 -2
- package/public/guide.html +33 -13
- package/public/index.html +469 -111
- package/public/learn.html +183 -18
- package/public/lessons.html +168 -10
- package/public/numbers.html +7 -7
- package/public/pro.html +34 -11
- package/scripts/agent-memory-lifecycle.js +211 -0
- package/scripts/agent-readiness.js +20 -3
- package/scripts/agent-reward-model.js +53 -1
- package/scripts/auto-promote-gates.js +82 -10
- package/scripts/auto-wire-hooks.js +14 -0
- package/scripts/billing.js +93 -1
- package/scripts/bot-detection.js +61 -3
- package/scripts/build-metadata.js +50 -10
- package/scripts/cli-feedback.js +4 -2
- package/scripts/cli-schema.js +97 -0
- package/scripts/cli-telemetry.js +6 -1
- package/scripts/commercial-offer.js +82 -2
- package/scripts/context-manager.js +74 -6
- package/scripts/dashboard.js +68 -2
- package/scripts/export-databricks-bundle.js +5 -1
- package/scripts/export-dpo-pairs.js +7 -2
- package/scripts/feedback-loop.js +123 -1
- package/scripts/feedback-quality.js +87 -0
- package/scripts/filesystem-search.js +35 -10
- package/scripts/gate-stats.js +89 -0
- package/scripts/gates-engine.js +1176 -85
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-runtime.js +20 -14
- package/scripts/hook-thumbgate-cache-updater.js +18 -2
- package/scripts/hybrid-feedback-context.js +142 -7
- package/scripts/lesson-inference.js +8 -3
- package/scripts/lesson-search.js +17 -1
- package/scripts/license.js +10 -10
- package/scripts/llm-client.js +169 -4
- package/scripts/local-model-profile.js +15 -8
- package/scripts/mcp-config.js +7 -1
- package/scripts/memory-scope-readiness.js +159 -0
- package/scripts/meta-agent-loop.js +36 -0
- package/scripts/operational-integrity.js +39 -5
- package/scripts/oss-pr-opportunity-scout.js +35 -5
- package/scripts/plausible-server-events.js +9 -6
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +42 -1
- package/scripts/published-cli.js +0 -8
- package/scripts/rate-limiter.js +64 -13
- package/scripts/secret-scanner.js +44 -5
- package/scripts/security-scanner.js +260 -10
- package/scripts/self-distill-agent.js +3 -1
- package/scripts/seo-gsd.js +916 -7
- package/scripts/statusline-cache-path.js +17 -2
- package/scripts/statusline-local-stats.js +9 -1
- package/scripts/statusline-meta.js +28 -2
- package/scripts/statusline.sh +20 -4
- package/scripts/telemetry-analytics.js +357 -0
- package/scripts/thompson-sampling.js +31 -10
- package/scripts/thumbgate-bench.js +16 -1
- package/scripts/thumbgate-search.js +85 -19
- package/scripts/tool-registry.js +169 -1
- package/scripts/vector-store.js +45 -0
- package/scripts/workflow-sentinel.js +286 -53
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +2683 -319
- package/scripts/bot-detector.js +0 -50
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { loadOptionalModule } = require("./private-core-boundary");
|
|
5
|
+
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* Security Scanner — OWASP-aware static analysis for PreToolUse checks.
|
|
6
9
|
*
|
|
@@ -16,6 +19,10 @@
|
|
|
16
19
|
const fs = require('fs');
|
|
17
20
|
const path = require('path');
|
|
18
21
|
const { recordAuditEvent, auditToFeedback } = require('./audit-trail');
|
|
22
|
+
const { scanInstallCommand, detectSlopsquat } = loadOptionalModule('./slopsquat-guard', () => ({
|
|
23
|
+
scanInstallCommand: () => ({ detected: false, findings: [] }),
|
|
24
|
+
detectSlopsquat: () => null,
|
|
25
|
+
}));
|
|
19
26
|
|
|
20
27
|
// ---------------------------------------------------------------------------
|
|
21
28
|
// Vulnerability pattern definitions (OWASP Top 10 + supply chain)
|
|
@@ -30,6 +37,7 @@ const VULN_PATTERNS = [
|
|
|
30
37
|
label: 'Command injection via unsanitized input',
|
|
31
38
|
regex: /\bexec(?:Sync)?\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+\s*(?:req\.|input|args|params|query|body|user))/g,
|
|
32
39
|
fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
|
|
40
|
+
remediation: 'Avoid passing unsanitized input to child_process.exec/execSync. Use execFile or spawn with args passed as an array.',
|
|
33
41
|
},
|
|
34
42
|
{
|
|
35
43
|
id: 'shell-interpolation',
|
|
@@ -38,6 +46,7 @@ const VULN_PATTERNS = [
|
|
|
38
46
|
label: 'Shell command with string interpolation',
|
|
39
47
|
regex: /\bexec(?:Sync)?\s*\(\s*`[^`]*\$\{[^}]*(?:req\.|input|args|params|query|body|user|process\.env)/g,
|
|
40
48
|
fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
|
|
49
|
+
remediation: 'Use child_process.execFile or child_process.spawn with an array of arguments to avoid shell command interpolation.',
|
|
41
50
|
},
|
|
42
51
|
{
|
|
43
52
|
id: 'sql-injection',
|
|
@@ -46,6 +55,7 @@ const VULN_PATTERNS = [
|
|
|
46
55
|
label: 'Potential SQL injection via string concatenation',
|
|
47
56
|
regex: /(?:query|execute|run|all|get)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+\s*(?:req\.|input|args|params|query|body|user))/g,
|
|
48
57
|
fileTypes: ['.js', '.ts', '.mjs', '.cjs', '.py'],
|
|
58
|
+
remediation: 'Use parameterized queries or prepared statements instead of dynamic SQL string concatenation.',
|
|
49
59
|
},
|
|
50
60
|
{
|
|
51
61
|
id: 'eval-usage',
|
|
@@ -54,6 +64,7 @@ const VULN_PATTERNS = [
|
|
|
54
64
|
label: 'Dynamic code execution (eval/Function constructor)',
|
|
55
65
|
regex: /\b(?:eval|new\s+Function)\s*\([^)]*(?:req\.|input|args|params|query|body|user)/g,
|
|
56
66
|
fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
|
|
67
|
+
remediation: 'Use JSON.parse() or a safe parser library instead of eval() or dynamic Function constructors.',
|
|
57
68
|
},
|
|
58
69
|
|
|
59
70
|
// XSS
|
|
@@ -64,6 +75,7 @@ const VULN_PATTERNS = [
|
|
|
64
75
|
label: 'Potential XSS via innerHTML assignment',
|
|
65
76
|
regex: /\.innerHTML\s*=\s*(?!['"]<(?:div|span|p|br|hr)\s*\/?>['"])/g,
|
|
66
77
|
fileTypes: ['.js', '.ts', '.jsx', '.tsx', '.mjs'],
|
|
78
|
+
remediation: 'Use element.textContent or element.innerText instead of innerHTML to prevent cross-site scripting (XSS).',
|
|
67
79
|
},
|
|
68
80
|
{
|
|
69
81
|
id: 'xss-dangerously-set',
|
|
@@ -72,6 +84,7 @@ const VULN_PATTERNS = [
|
|
|
72
84
|
label: 'React dangerouslySetInnerHTML with dynamic content',
|
|
73
85
|
regex: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*(?!['"])/g,
|
|
74
86
|
fileTypes: ['.jsx', '.tsx', '.js', '.ts'],
|
|
87
|
+
remediation: 'Ensure dynamic content passed to dangerouslySetInnerHTML is sanitized using DOMPurify or equivalent.',
|
|
75
88
|
},
|
|
76
89
|
|
|
77
90
|
// Path traversal
|
|
@@ -82,6 +95,7 @@ const VULN_PATTERNS = [
|
|
|
82
95
|
label: 'Path traversal via unsanitized user input',
|
|
83
96
|
regex: /path\.(?:join|resolve)\s*\([^)]*(?:req\.|input|args|params|query|body|user)/g,
|
|
84
97
|
fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
|
|
98
|
+
remediation: 'Sanitize path input using path.basename() or validate the path against an explicit list of allowed directories.',
|
|
85
99
|
},
|
|
86
100
|
{
|
|
87
101
|
id: 'path-traversal-direct',
|
|
@@ -90,6 +104,7 @@ const VULN_PATTERNS = [
|
|
|
90
104
|
label: 'Direct file read with user-controlled path',
|
|
91
105
|
regex: /fs\.(?:readFile(?:Sync)?|createReadStream)\s*\(\s*(?:req\.|input|args|params|query|body|user)/g,
|
|
92
106
|
fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
|
|
107
|
+
remediation: 'Validate input paths and restrict file system access to a sandboxed directory.',
|
|
93
108
|
},
|
|
94
109
|
|
|
95
110
|
// Prototype pollution
|
|
@@ -100,6 +115,7 @@ const VULN_PATTERNS = [
|
|
|
100
115
|
label: 'Potential prototype pollution via recursive merge',
|
|
101
116
|
regex: /(?:__proto__|constructor\s*\[\s*['"]prototype['"]\s*\]|Object\.assign\s*\(\s*\{\s*\})/g,
|
|
102
117
|
fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
|
|
118
|
+
remediation: 'Avoid recursive object mergers without checking for __proto__ or constructor keys, or use Object.create(null).',
|
|
103
119
|
},
|
|
104
120
|
|
|
105
121
|
// Insecure crypto
|
|
@@ -110,6 +126,7 @@ const VULN_PATTERNS = [
|
|
|
110
126
|
label: 'Weak hash algorithm (MD5/SHA1) for security use',
|
|
111
127
|
regex: /createHash\s*\(\s*['"](?:md5|sha1)['"]\s*\)/gi,
|
|
112
128
|
fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
|
|
129
|
+
remediation: 'Use SHA-256 or SHA-512 (e.g. crypto.createHash("sha256")) instead of MD5 or SHA-1 for security use cases.',
|
|
113
130
|
},
|
|
114
131
|
{
|
|
115
132
|
id: 'hardcoded-secret',
|
|
@@ -118,6 +135,7 @@ const VULN_PATTERNS = [
|
|
|
118
135
|
label: 'Hardcoded secret/password in source code',
|
|
119
136
|
regex: /(?:password|secret|apiKey|api_key|token)\s*[:=]\s*['"][A-Za-z0-9+/=_-]{12,}['"]/g,
|
|
120
137
|
fileTypes: ['.js', '.ts', '.mjs', '.cjs', '.py', '.go', '.java'],
|
|
138
|
+
remediation: 'Move hardcoded secrets to environment variables or use a secure secrets manager.',
|
|
121
139
|
},
|
|
122
140
|
|
|
123
141
|
// SSRF
|
|
@@ -128,6 +146,7 @@ const VULN_PATTERNS = [
|
|
|
128
146
|
label: 'Potential SSRF via user-controlled URL',
|
|
129
147
|
regex: /(?:fetch|axios|got|request|https?\.(?:get|request))\s*\(\s*(?:`[^`]*\$\{|(?:req\.|input|args|params|query|body|user))/g,
|
|
130
148
|
fileTypes: ['.js', '.ts', '.mjs', '.cjs'],
|
|
149
|
+
remediation: 'Validate the destination host against a strict whitelist of allowed domains and block requests to internal IPs.',
|
|
131
150
|
},
|
|
132
151
|
|
|
133
152
|
// Insecure deserialization
|
|
@@ -138,6 +157,16 @@ const VULN_PATTERNS = [
|
|
|
138
157
|
label: 'Unsafe deserialization of untrusted data',
|
|
139
158
|
regex: /(?:unserialize|yaml\.load\s*\((?!.*Loader\s*=\s*yaml\.SafeLoader)|pickle\.loads?|Marshal\.load)/g,
|
|
140
159
|
fileTypes: ['.js', '.ts', '.py', '.rb'],
|
|
160
|
+
remediation: 'Use safe parsing libraries or options (such as yaml.safeLoad) instead of unsafe deserialization/eval-like loaders.',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'badhost-url-confusion',
|
|
164
|
+
category: 'host-header',
|
|
165
|
+
severity: 'high',
|
|
166
|
+
label: 'Potential BadHost-style host or URL confusion in AI service',
|
|
167
|
+
regex: /\b(?:request\.url(?:\.path)?|url_for\s*\([^)]*_external\s*=\s*True|headers\s*\[\s*['"](?:host|x-forwarded-host)['"]\s*\])/gi,
|
|
168
|
+
fileTypes: ['.py'],
|
|
169
|
+
remediation: 'Verify Host and X-Forwarded-Host headers against an approved whitelist before using them for routing or URL generation.',
|
|
141
170
|
},
|
|
142
171
|
];
|
|
143
172
|
|
|
@@ -151,6 +180,7 @@ const SUPPLY_CHAIN_PATTERNS = [
|
|
|
151
180
|
category: 'supply-chain',
|
|
152
181
|
severity: 'high',
|
|
153
182
|
label: 'Potentially typosquatted package name',
|
|
183
|
+
remediation: 'Verify spelling and authenticity of package name. If typosquatted, run: npm uninstall <package>.',
|
|
154
184
|
// Common typosquat indicators: single-char substitutions of popular packages
|
|
155
185
|
knownSafe: new Set([
|
|
156
186
|
'express', 'lodash', 'axios', 'react', 'vue', 'angular', 'moment',
|
|
@@ -164,6 +194,7 @@ const SUPPLY_CHAIN_PATTERNS = [
|
|
|
164
194
|
category: 'supply-chain',
|
|
165
195
|
severity: 'critical',
|
|
166
196
|
label: 'Suspicious install script in package.json',
|
|
197
|
+
remediation: 'Remove the pre/postinstall script, or run package installation with --ignore-scripts.',
|
|
167
198
|
regex: /["'](?:pre|post)?install["']\s*:\s*["'](?:.*(?:curl|wget|nc\s|bash\s|sh\s|eval|exec|child_process))/g,
|
|
168
199
|
},
|
|
169
200
|
{
|
|
@@ -171,10 +202,79 @@ const SUPPLY_CHAIN_PATTERNS = [
|
|
|
171
202
|
category: 'supply-chain',
|
|
172
203
|
severity: 'medium',
|
|
173
204
|
label: 'Wildcard or latest version in dependency',
|
|
205
|
+
remediation: 'Specify a concrete version constraint (e.g., "^1.0.0") instead of a wildcard or latest.',
|
|
174
206
|
regex: /["'](?:dependencies|devDependencies|peerDependencies)["'][\s\S]{0,500}?["'][^"']+["']\s*:\s*["'](?:\*|latest|>=)/g,
|
|
175
207
|
},
|
|
176
208
|
];
|
|
177
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Simple static analysis check (reachability/usage check) to see if a package is imported.
|
|
212
|
+
* @param {string} pkg - The package name to search for
|
|
213
|
+
* @param {string} rootDir - Root directory to walk
|
|
214
|
+
* @returns {boolean}
|
|
215
|
+
*/
|
|
216
|
+
function isPackageImported(pkg, rootDir = process.cwd()) {
|
|
217
|
+
const IGNORED_DIRS = new Set([
|
|
218
|
+
'node_modules', '.git', 'dist', 'coverage', '.planning', '.artifacts', '.gemini', '.antigravitycli'
|
|
219
|
+
]);
|
|
220
|
+
const FILE_EXTS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.py']);
|
|
221
|
+
|
|
222
|
+
// Escape package name for regex
|
|
223
|
+
const escapedPkg = pkg.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
224
|
+
const importRegex = new RegExp(
|
|
225
|
+
`(?:require\\s*\\(\\s*['"]${escapedPkg}['"]\\s*\\)|from\\s*['"]${escapedPkg}['"]|import\\s*\\(\\s*['"]${escapedPkg}['"]\\s*\\)|import\\s+['"]${escapedPkg}['"])`,
|
|
226
|
+
'i'
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
let found = false;
|
|
230
|
+
let fileCount = 0;
|
|
231
|
+
const maxFiles = 200; // safety limit to keep it fast (<50ms)
|
|
232
|
+
|
|
233
|
+
function walk(dir) {
|
|
234
|
+
if (found || fileCount >= maxFiles) return;
|
|
235
|
+
let files;
|
|
236
|
+
try {
|
|
237
|
+
files = fs.readdirSync(dir);
|
|
238
|
+
} catch {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const file of files) {
|
|
243
|
+
if (found || fileCount >= maxFiles) return;
|
|
244
|
+
const fullPath = path.join(dir, file);
|
|
245
|
+
let stat;
|
|
246
|
+
try {
|
|
247
|
+
stat = fs.statSync(fullPath);
|
|
248
|
+
} catch {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (stat.isDirectory()) {
|
|
253
|
+
if (!IGNORED_DIRS.has(file)) {
|
|
254
|
+
walk(fullPath);
|
|
255
|
+
}
|
|
256
|
+
} else if (stat.isFile()) {
|
|
257
|
+
const ext = path.extname(file).toLowerCase();
|
|
258
|
+
if (FILE_EXTS.has(ext)) {
|
|
259
|
+
fileCount++;
|
|
260
|
+
try {
|
|
261
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
262
|
+
if (importRegex.test(content)) {
|
|
263
|
+
found = true;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
// ignore read errors
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
walk(rootDir);
|
|
275
|
+
return found;
|
|
276
|
+
}
|
|
277
|
+
|
|
178
278
|
// ---------------------------------------------------------------------------
|
|
179
279
|
// Core scanning functions
|
|
180
280
|
// ---------------------------------------------------------------------------
|
|
@@ -212,6 +312,7 @@ function scanCode(content, filePath = '') {
|
|
|
212
312
|
line: lineNumber,
|
|
213
313
|
match: match[0].slice(0, 120),
|
|
214
314
|
path: filePath,
|
|
315
|
+
remediation: pattern.remediation,
|
|
215
316
|
});
|
|
216
317
|
// Only report first match per pattern per file to avoid noise
|
|
217
318
|
break;
|
|
@@ -224,6 +325,22 @@ function scanCode(content, filePath = '') {
|
|
|
224
325
|
};
|
|
225
326
|
}
|
|
226
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Scan Python / AI-service code for BadHost-style URL and host-header confusion.
|
|
330
|
+
* This is deliberately narrow and evidence-oriented: it does not claim a CVE,
|
|
331
|
+
* it flags code that should prove canonical host handling before deployment.
|
|
332
|
+
* @param {string} content
|
|
333
|
+
* @param {string} filePath
|
|
334
|
+
* @returns {{ detected: boolean, findings: Array<Object> }}
|
|
335
|
+
*/
|
|
336
|
+
function scanBadHostExposure(content, filePath = '') {
|
|
337
|
+
const result = scanCode(content, filePath);
|
|
338
|
+
return {
|
|
339
|
+
detected: result.findings.some((finding) => finding.id === 'badhost-url-confusion'),
|
|
340
|
+
findings: result.findings.filter((finding) => finding.id === 'badhost-url-confusion'),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
227
344
|
/**
|
|
228
345
|
* Scan dependency changes in package.json mutations.
|
|
229
346
|
* @param {string} oldContent - Previous package.json content (empty string if new file)
|
|
@@ -256,6 +373,10 @@ function scanDependencyChange(oldContent, newContent) {
|
|
|
256
373
|
|
|
257
374
|
for (const [pkg, version] of Object.entries(newDeps)) {
|
|
258
375
|
if (!oldDeps[pkg]) {
|
|
376
|
+
// Run usage check (reachability)
|
|
377
|
+
const reachable = isPackageImported(pkg);
|
|
378
|
+
const reachability = reachable ? 'imported' : 'unimported';
|
|
379
|
+
|
|
259
380
|
// New dependency added — check for red flags
|
|
260
381
|
if (version === '*' || version === 'latest' || version.startsWith('>=')) {
|
|
261
382
|
findings.push({
|
|
@@ -264,6 +385,9 @@ function scanDependencyChange(oldContent, newContent) {
|
|
|
264
385
|
severity: 'medium',
|
|
265
386
|
label: `Wildcard version for new dependency: ${pkg}@${version}`,
|
|
266
387
|
path: 'package.json',
|
|
388
|
+
remediation: `Specify a concrete version constraint (e.g., "^${version === '*' || version === 'latest' ? '1.0.0' : version}") instead of a wildcard.`,
|
|
389
|
+
reachable,
|
|
390
|
+
reachability,
|
|
267
391
|
});
|
|
268
392
|
}
|
|
269
393
|
|
|
@@ -275,6 +399,24 @@ function scanDependencyChange(oldContent, newContent) {
|
|
|
275
399
|
severity: 'high',
|
|
276
400
|
label: `Suspiciously short package name: "${pkg}"`,
|
|
277
401
|
path: 'package.json',
|
|
402
|
+
remediation: 'Double-check spelling and verify this package is not a typosquatting attempt.',
|
|
403
|
+
reachable,
|
|
404
|
+
reachability,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Tier 3: Slopsquat Guard — deterministic typosquat detection
|
|
409
|
+
const slopsquatFinding = detectSlopsquat(pkg, 'npm');
|
|
410
|
+
if (slopsquatFinding) {
|
|
411
|
+
findings.push({
|
|
412
|
+
id: slopsquatFinding.id,
|
|
413
|
+
category: 'supply-chain',
|
|
414
|
+
severity: slopsquatFinding.severity,
|
|
415
|
+
label: slopsquatFinding.label,
|
|
416
|
+
path: 'package.json',
|
|
417
|
+
remediation: `Verify spelling and authenticity of package "${pkg}". If typosquatted, run: npm uninstall ${pkg}.`,
|
|
418
|
+
reachable,
|
|
419
|
+
reachability,
|
|
278
420
|
});
|
|
279
421
|
}
|
|
280
422
|
}
|
|
@@ -292,6 +434,7 @@ function scanDependencyChange(oldContent, newContent) {
|
|
|
292
434
|
severity: 'critical',
|
|
293
435
|
label: `Suspicious install script: ${name} → ${cmd.slice(0, 80)}`,
|
|
294
436
|
path: 'package.json',
|
|
437
|
+
remediation: 'Remove the suspicious pre/postinstall script, or run package installation with --ignore-scripts.',
|
|
295
438
|
});
|
|
296
439
|
}
|
|
297
440
|
}
|
|
@@ -313,27 +456,68 @@ function scanDependencyChange(oldContent, newContent) {
|
|
|
313
456
|
* @param {Object} input - Hook input { tool_name, tool_input }
|
|
314
457
|
* @returns {Object|null} Gate result or null if clean
|
|
315
458
|
*/
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Evaluate slopsquat guard for a Bash command.
|
|
462
|
+
* @param {string} toolName
|
|
463
|
+
* @param {Object} toolInput
|
|
464
|
+
* @returns {Object|null}
|
|
465
|
+
*/
|
|
466
|
+
function evaluateSlopsquatScan(toolName, toolInput) {
|
|
467
|
+
if (toolName !== "Bash") return null;
|
|
468
|
+
const command = toolInput.command || "";
|
|
469
|
+
if (!command) return null;
|
|
470
|
+
|
|
471
|
+
const { resolveMode, scanInstallCommand } = loadOptionalModule("./slopsquat-guard", () => ({
|
|
472
|
+
resolveMode: () => "block",
|
|
473
|
+
scanInstallCommand: () => ({ detected: false, findings: [] }),
|
|
474
|
+
}));
|
|
475
|
+
|
|
476
|
+
const mode = resolveMode();
|
|
477
|
+
if (mode === "off") return null;
|
|
478
|
+
|
|
479
|
+
const result = scanInstallCommand(command);
|
|
480
|
+
if (!result.detected) return null;
|
|
481
|
+
|
|
482
|
+
const hasCritical = result.findings.some(f => f.severity === "critical");
|
|
483
|
+
const decision = (mode === "block" && hasCritical) ? "deny" : "warn";
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
decision,
|
|
487
|
+
gate: "slopsquat-guard",
|
|
488
|
+
message: "✗ THUMBGATE: " + result.findings[0].label,
|
|
489
|
+
severity: hasCritical ? "critical" : "high",
|
|
490
|
+
reasoning: result.findings.map(f => f.label),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
316
494
|
function evaluateSecurityScan(input = {}) {
|
|
317
495
|
const toolName = input.tool_name || input.toolName || '';
|
|
318
496
|
const toolInput = input.tool_input || {};
|
|
319
497
|
|
|
320
|
-
// Only scan write-type operations
|
|
498
|
+
// Only scan write-type operations and Bash commands
|
|
321
499
|
const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
322
|
-
|
|
500
|
+
const IS_BASH = toolName === 'Bash';
|
|
501
|
+
|
|
502
|
+
if (!WRITE_TOOLS.has(toolName) && !IS_BASH) {
|
|
323
503
|
return null;
|
|
324
504
|
}
|
|
325
505
|
|
|
326
506
|
const filePath = toolInput.file_path || toolInput.path || '';
|
|
327
507
|
const content = toolInput.content || toolInput.new_string || '';
|
|
508
|
+
const command = toolInput.command || '';
|
|
328
509
|
|
|
329
|
-
if (!content) return null;
|
|
510
|
+
if (!content && !command) return null;
|
|
330
511
|
|
|
331
|
-
// Tier 1: Code vulnerability scan
|
|
332
|
-
|
|
512
|
+
// Tier 1: Code vulnerability scan (for Edits)
|
|
513
|
+
let codeResult = { detected: false, findings: [] };
|
|
514
|
+
if (content) {
|
|
515
|
+
codeResult = scanCode(content, filePath);
|
|
516
|
+
}
|
|
333
517
|
|
|
334
518
|
// Tier 2: Supply chain scan for package.json changes
|
|
335
519
|
let supplyChainResult = { detected: false, findings: [] };
|
|
336
|
-
if (filePath && path.basename(filePath) === 'package.json') {
|
|
520
|
+
if (filePath && path.basename(filePath) === 'package.json' && content) {
|
|
337
521
|
let oldContent = '';
|
|
338
522
|
try {
|
|
339
523
|
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
|
|
@@ -344,7 +528,14 @@ function evaluateSecurityScan(input = {}) {
|
|
|
344
528
|
supplyChainResult = scanDependencyChange(oldContent, content);
|
|
345
529
|
}
|
|
346
530
|
|
|
347
|
-
|
|
531
|
+
// Tier 3: Slopsquat Guard for Bash commands
|
|
532
|
+
let slopsquatResult = { detected: false, findings: [] };
|
|
533
|
+
if (IS_BASH && command) {
|
|
534
|
+
const slopsquatGate = evaluateSlopsquatScan(toolName, toolInput);
|
|
535
|
+
if (slopsquatGate) return slopsquatGate;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const allFindings = [...codeResult.findings, ...supplyChainResult.findings, ...slopsquatResult.findings];
|
|
348
539
|
if (allFindings.length === 0) return null;
|
|
349
540
|
|
|
350
541
|
// Determine overall severity
|
|
@@ -359,16 +550,18 @@ function evaluateSecurityScan(input = {}) {
|
|
|
359
550
|
`[${f.severity.toUpperCase()}] ${f.label}${f.line ? ` (line ${f.line})` : ''}`
|
|
360
551
|
).join('; ');
|
|
361
552
|
|
|
362
|
-
const message = `Security scan detected ${allFindings.length} issue(s) in ${filePath || 'code'}: ${summary}`;
|
|
553
|
+
const message = `Security scan detected ${allFindings.length} issue(s) in ${filePath || (IS_BASH ? 'command' : 'code')}: ${summary}`;
|
|
363
554
|
|
|
364
555
|
const reasoning = [
|
|
365
|
-
|
|
556
|
+
IS_BASH
|
|
557
|
+
? `Scanned Bash command for slopsquat/typosquat risk: "${command.slice(0, 100)}..."`
|
|
558
|
+
: `Scanned ${content.length} bytes of content being written to ${filePath || 'unknown file'}`,
|
|
366
559
|
...allFindings.map(f => `${f.category}/${f.id}: ${f.label}${f.match ? ` — matched: ${f.match.slice(0, 60)}` : ''}`),
|
|
367
560
|
];
|
|
368
561
|
|
|
369
562
|
recordAuditEvent({
|
|
370
563
|
toolName,
|
|
371
|
-
toolInput: { file_path: filePath, content_length: content.length },
|
|
564
|
+
toolInput: { file_path: filePath, content_length: content.length, command: IS_BASH ? command : undefined },
|
|
372
565
|
decision,
|
|
373
566
|
gateId,
|
|
374
567
|
message,
|
|
@@ -434,15 +627,72 @@ function scanGitDiff(diffContent) {
|
|
|
434
627
|
};
|
|
435
628
|
}
|
|
436
629
|
|
|
630
|
+
function buildThreatDefensePlaybook(scanResult = {}, options = {}) {
|
|
631
|
+
const findings = Array.isArray(scanResult.findings)
|
|
632
|
+
? scanResult.findings
|
|
633
|
+
: (scanResult.securityScan && Array.isArray(scanResult.securityScan.findings) ? scanResult.securityScan.findings : []);
|
|
634
|
+
const critical = findings.filter((finding) => finding.severity === 'critical');
|
|
635
|
+
const high = findings.filter((finding) => finding.severity === 'high');
|
|
636
|
+
const categories = Array.from(new Set(findings.map((finding) => finding.category).filter(Boolean)));
|
|
637
|
+
const hasFindings = findings.length > 0;
|
|
638
|
+
const hasPatchEvidence = Boolean(options.patchEvidence || options.testEvidence || options.ciEvidence);
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
name: 'thumbgate-ai-threat-defense-playbook',
|
|
642
|
+
status: critical.length > 0 ? 'block' : high.length > 0 ? 'remediate' : 'monitor',
|
|
643
|
+
phases: [
|
|
644
|
+
{
|
|
645
|
+
id: 'prepare',
|
|
646
|
+
action: 'harden-foundation',
|
|
647
|
+
evidence: ['gate templates enabled', 'protected files configured', 'rollback path documented'],
|
|
648
|
+
required: true,
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
id: 'scan-prioritize',
|
|
652
|
+
action: hasFindings ? 'prioritize detected security findings by severity and exploit surface' : 'keep posture scan active',
|
|
653
|
+
evidence: categories.length ? categories : ['clean scan'],
|
|
654
|
+
required: true,
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
id: 'remediate',
|
|
658
|
+
action: hasFindings ? 'patch, run focused tests, and re-scan before allowing risky agent actions' : 'no remediation required from current scan',
|
|
659
|
+
evidence: hasPatchEvidence ? ['patch evidence present'] : ['patch diff', 'focused test output', 'repeat scan'],
|
|
660
|
+
required: hasFindings,
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
id: 'monitor',
|
|
664
|
+
action: 'record audit event and keep continuous detection enabled for future tool calls',
|
|
665
|
+
evidence: ['audit trail event', 'gate stats', 'review checkpoint'],
|
|
666
|
+
required: true,
|
|
667
|
+
},
|
|
668
|
+
],
|
|
669
|
+
priority: {
|
|
670
|
+
critical: critical.length,
|
|
671
|
+
high: high.length,
|
|
672
|
+
total: findings.length,
|
|
673
|
+
categories,
|
|
674
|
+
},
|
|
675
|
+
gateDecision: critical.length > 0 ? 'deny' : high.length > 0 ? 'warn' : 'allow',
|
|
676
|
+
nextActions: critical.length > 0
|
|
677
|
+
? ['Block the action', 'Patch the critical finding', 'Run focused tests', 'Re-scan the diff before retry']
|
|
678
|
+
: high.length > 0
|
|
679
|
+
? ['Warn the operator', 'Create a remediation task', 'Run focused tests', 'Monitor for repeat findings']
|
|
680
|
+
: ['Keep continuous scan enabled', 'Review checkpoint metrics after the next session'],
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
437
684
|
// ---------------------------------------------------------------------------
|
|
438
685
|
// Exports
|
|
439
686
|
// ---------------------------------------------------------------------------
|
|
440
687
|
|
|
441
688
|
module.exports = {
|
|
689
|
+
evaluateSlopsquatScan,
|
|
442
690
|
VULN_PATTERNS,
|
|
443
691
|
SUPPLY_CHAIN_PATTERNS,
|
|
444
692
|
scanCode,
|
|
693
|
+
scanBadHostExposure,
|
|
445
694
|
scanDependencyChange,
|
|
446
695
|
evaluateSecurityScan,
|
|
447
696
|
scanGitDiff,
|
|
697
|
+
buildThreatDefensePlaybook,
|
|
448
698
|
};
|
|
@@ -26,6 +26,7 @@ const { resolveFeedbackDir } = require('./feedback-paths');
|
|
|
26
26
|
const { createLesson, inferStructuredLesson } = require('./lesson-inference');
|
|
27
27
|
const { buildStableId } = require('./conversation-context');
|
|
28
28
|
const { ensureParentDir, readJsonl } = require('./fs-utils');
|
|
29
|
+
const { redactSecretsDeep } = require('./secret-redaction');
|
|
29
30
|
|
|
30
31
|
const HOME = process.env.HOME || process.env.USERPROFILE || os.homedir() || '';
|
|
31
32
|
const SELF_DISTILL_RUNS_PATH = path.join(HOME, '.thumbgate', 'self-distill-runs.jsonl');
|
|
@@ -383,7 +384,8 @@ async function generateLlmLessons(conversationWindow, model) {
|
|
|
383
384
|
|
|
384
385
|
function writeRunManifest(manifest) {
|
|
385
386
|
ensureParentDir(SELF_DISTILL_RUNS_PATH);
|
|
386
|
-
|
|
387
|
+
// Redact secrets — the manifest embeds lesson triggers/actions distilled from conversation text.
|
|
388
|
+
fs.appendFileSync(SELF_DISTILL_RUNS_PATH, JSON.stringify(redactSecretsDeep(manifest)) + '\n');
|
|
387
389
|
}
|
|
388
390
|
|
|
389
391
|
function readRunManifests() {
|