@lhi/tdd-audit 1.16.0 → 1.20.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/lib/badge.js CHANGED
@@ -8,6 +8,24 @@ const BADGE_MARKER = 'tdd-audit-badge';
8
8
 
9
9
  const NPM_URL = 'https://www.npmjs.com/package/@lhi/tdd-audit';
10
10
 
11
+ /**
12
+ * Validate that a URL uses http: or https:. Returns the URL if safe, NPM_URL otherwise.
13
+ * Prevents javascript:, data:, file:, and protocol-relative URLs from landing in
14
+ * badge links or SARIF output.
15
+ * @param {string} raw
16
+ * @returns {string}
17
+ */
18
+ function safeSiteUrl(raw) {
19
+ if (!raw || !raw.trim()) return NPM_URL;
20
+ try {
21
+ const parsed = new URL(raw.trim());
22
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return NPM_URL;
23
+ return raw.trim();
24
+ } catch {
25
+ return NPM_URL;
26
+ }
27
+ }
28
+
11
29
  /**
12
30
  * Build a shields.io badge markdown line reflecting actual scan results.
13
31
  *
@@ -22,11 +40,15 @@ const NPM_URL = 'https://www.npmjs.com/package/@lhi/tdd-audit';
22
40
  * no config file exists — the link falls back to the @lhi/tdd-audit npm page
23
41
  * so readers always know where the security tooling came from.
24
42
  *
25
- * @param {Array} findings - findings array returned by quickScan()
26
- * @param {string} [siteUrl] - optional override link (from config.tdd_site)
27
- * @returns {string} - single-line markdown badge ending with \n
43
+ * The badge label text defaults to "tdd-audit" but can be overridden via
44
+ * `badge_label` in .tdd-audit.json useful for white-labeled distributions.
45
+ *
46
+ * @param {Array} findings - findings array returned by quickScan()
47
+ * @param {string} [siteUrl] - optional override link (from config.tdd_site)
48
+ * @param {string} [label] - optional badge label (from config.badge_label)
49
+ * @returns {string} - single-line markdown badge ending with \n
28
50
  */
29
- function badgeLine(findings, siteUrl) {
51
+ function badgeLine(findings, siteUrl, label) {
30
52
  // Exclude test-file findings and likely false positives — badge reflects production code only
31
53
  const real = (findings || []).filter(f => !f.likelyFalsePositive && !f.inTestFile);
32
54
  const criticals = real.filter(f => f.severity === 'CRITICAL').length;
@@ -44,11 +66,16 @@ function badgeLine(findings, siteUrl) {
44
66
  color = 'brightgreen';
45
67
  }
46
68
 
47
- const badgeUrl = `https://img.shields.io/badge/tdd--audit-${message}-${color}`;
48
- const targetUrl = (siteUrl && siteUrl.trim()) ? siteUrl.trim() : NPM_URL;
69
+ const rawLabel = (label && label.trim()) ? label.trim() : 'tdd-audit';
70
+ // Strip markdown link-breaking characters before embedding in alt text or URL
71
+ const badgeLabel = rawLabel.replace(/[[\]()]/g, '');
72
+ // Encode the label for use in a shields.io URL (spaces → %20, hyphens → --)
73
+ const encodedLabel = badgeLabel.replace(/ /g, '%20').replace(/-/g, '--');
74
+ const badgeUrl = `https://img.shields.io/badge/${encodedLabel}-${message}-${color}`;
75
+ const targetUrl = safeSiteUrl(siteUrl);
49
76
  // Embed the marker as a hidden HTML comment after the badge so injectBadge()
50
77
  // can locate and replace the line on subsequent runs.
51
- return `[![tdd-audit](${badgeUrl})](${targetUrl}) <!-- ${BADGE_MARKER} -->\n`;
78
+ return `[![${badgeLabel}](${badgeUrl})](${targetUrl}) <!-- ${BADGE_MARKER} -->\n`;
52
79
  }
53
80
 
54
81
  /**
package/lib/config.js CHANGED
@@ -18,6 +18,47 @@ const DEFAULTS = {
18
18
  serverApiKey: null, // key required on REST API calls
19
19
  trustProxy: false, // trust X-Forwarded-For for rate limiting
20
20
  tdd_site: null, // custom URL for the README badge link; falls back to npm page
21
+
22
+ // Branding — for wrapper/rebranded distributions
23
+ org: null, // org name in reports, SECURITY.md, and pattern PRs
24
+ project: null, // project name in reports and pattern contribution branch names
25
+ badge_label: null, // badge label text; defaults to 'tdd-audit'
26
+ security_name: null, // name of the security contact (stamped into SECURITY.md, compliance reports, and webhook payloads)
27
+ security_email: null, // email of the security contact (used as the vulnerability reporting address in SECURITY.md)
28
+
29
+ // Extensibility — both the CLI and the Claude Code skill honour these
30
+ pattern_repos: [], // [{name, url, local_path, namespace}] — RAG-indexed at startup
31
+ extra_skill_dirs: [], // relative paths to extra Claude Code skill directories
32
+ extra_repos: [], // [{url, local_path}] — cloned/pulled for reference
33
+ mcp_services: [], // [{name, cwd, command, args}] — started before first agent turn
34
+ extra_domains: [], // [{name, prompt_file}] — custom audit domains
35
+
36
+ // Policy as code — org-level severity overrides
37
+ // e.g. { "CORS Wildcard": "CRITICAL", "Sensitive Log": "HIGH" }
38
+ severity_overrides: {},
39
+
40
+ // Notifications — fire on scan complete
41
+ webhook_url: null, // POST findings JSON to this URL on scan complete
42
+ slack_webhook: null, // Slack incoming webhook URL for findings summary
43
+ slack_channel: null, // override default channel for the Slack webhook
44
+
45
+ // Workflow integration
46
+ open_pr: false, // open a GitHub PR per finding instead of committing directly
47
+ github_token: null, // token for PR creation; falls back to GITHUB_TOKEN env var
48
+ github_repo: null, // 'owner/repo' for PR creation; auto-detected from git remote if null
49
+
50
+ // Scheduled / CI modes
51
+ schedule: null, // cron expression — used by external schedulers, not the CLI itself
52
+ pr_mode: false, // lightweight scan only (no agents, no RAG) — designed for CI PR gates
53
+ org_scan: null, // GitHub org name — scan all repos in the org
54
+
55
+ // Output additions
56
+ sbom: false, // generate a CycloneDX SBOM alongside the audit report
57
+ report: false, // generate a human-readable compliance report (PDF/markdown)
58
+ watch: false, // re-scan affected files on change (watch mode)
59
+
60
+ // Secret rotation — when a hardcoded key is found, offer to rotate via provider API
61
+ rotate_secrets: false, // prompt to rotate detected secrets via provider API
21
62
  };
22
63
 
23
64
  // Provider-specific defaults for `tdd-audit init --provider <name>`
@@ -117,7 +158,15 @@ function parseCliOverrides(args) {
117
158
  const baseUrl = get('--base-url'); if (baseUrl) overrides.baseUrl = baseUrl;
118
159
  const format = get('--format'); if (format) overrides.output = format;
119
160
  const srvKey = get('--api-key'); if (srvKey) overrides.serverApiKey = srvKey;
120
- if (args.includes('--json')) overrides.output = 'json';
161
+ const threshold = get('--threshold'); if (threshold) overrides.severityThreshold = threshold;
162
+ const org = get('--org'); if (org) overrides.org_scan = org;
163
+ if (args.includes('--json')) overrides.output = 'json';
164
+ if (args.includes('--pr')) overrides.pr_mode = true;
165
+ if (args.includes('--open-pr')) overrides.open_pr = true;
166
+ if (args.includes('--sbom')) overrides.sbom = true;
167
+ if (args.includes('--watch')) overrides.watch = true;
168
+ if (args.includes('--report')) overrides.report = true;
169
+ if (args.includes('--rotate-secrets')) overrides.rotate_secrets = true;
121
170
  return overrides;
122
171
  }
123
172
 
package/lib/github.js CHANGED
@@ -90,4 +90,4 @@ function parseRepo(repoStr) {
90
90
  return { owner, repo };
91
91
  }
92
92
 
93
- module.exports = { uploadSarif, postReviewComments, parseRepo };
93
+ module.exports = { uploadSarif, postReviewComments, parseRepo, ghFetch };
package/lib/plugin.js CHANGED
@@ -4,9 +4,8 @@ const crypto = require('crypto');
4
4
  const path = require('path');
5
5
  const Fastify = require('fastify');
6
6
 
7
- const { quickScan } = require('./scanner');
8
- const { toJson, toSarif } = require('./reporter');
9
- const { remediate } = require('./remediator');
7
+ const { quickScan } = require('./scanner');
8
+ const { remediate } = require('./remediator');
10
9
  const { version } = require('../package.json');
11
10
  const {
12
11
  jobs, createJob, updateJob, subscribe, MAX_JOBS,
@@ -56,6 +55,26 @@ function createRateLimit() {
56
55
  };
57
56
  }
58
57
 
58
+ // ─── Webhook URL validation ───────────────────────────────────────────────────
59
+
60
+ const PRIVATE_IP = /^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|::1$|fc00:|fd)/;
61
+
62
+ /**
63
+ * Validate a user-supplied webhook URL before calling fetch().
64
+ * Accepts only https:// with a non-private hostname.
65
+ * Throws an Error describing the violation so callers can return 400.
66
+ */
67
+ function assertSafeWebhook(url) {
68
+ let parsed;
69
+ try { parsed = new URL(url); } catch { throw new Error('Invalid webhook URL'); }
70
+ if (parsed.protocol !== 'https:') {
71
+ throw new Error('webhook must use https://');
72
+ }
73
+ if (PRIVATE_IP.test(parsed.hostname) || parsed.hostname === 'localhost') {
74
+ throw new Error('webhook hostname is not allowed');
75
+ }
76
+ }
77
+
59
78
  // ─── Path validation ──────────────────────────────────────────────────────────
60
79
 
61
80
  function safeScanPath(rawPath) {
@@ -116,26 +135,6 @@ async function tddAuditPlugin(fastify, opts) {
116
135
  }
117
136
  });
118
137
 
119
- // ── POST /scan ──────────────────────────────────────────────────────────
120
- fastify.post('/scan', {
121
- config: { rawBody: false },
122
- }, async (request, reply) => {
123
- const body = request.body || {};
124
-
125
- let scanPath;
126
- try { scanPath = safeScanPath(body.path); }
127
- catch (e) { return reply.code(400).send({ error: e.message }); }
128
-
129
- const format = body.format || cfg.output || 'json';
130
- const t0 = Date.now();
131
- const findings = quickScan(scanPath);
132
- const exempted = findings.exempted || [];
133
- const duration = Date.now() - t0;
134
-
135
- if (format === 'sarif') return toSarif(findings, scanPath);
136
- return { ...toJson(findings, exempted), duration };
137
- });
138
-
139
138
  // ── POST /remediate ──────────────────────────────────────────────────────
140
139
  fastify.post('/remediate', async (request, reply) => {
141
140
  const body = request.body || {};
@@ -173,6 +172,11 @@ async function tddAuditPlugin(fastify, opts) {
173
172
  try { scanPath = safeScanPath(rawPath); }
174
173
  catch (e) { return reply.code(400).send({ error: e.message }); }
175
174
 
175
+ if (webhook) {
176
+ try { assertSafeWebhook(webhook); }
177
+ catch (e) { return reply.code(400).send({ error: `Invalid webhook: ${e.message}` }); }
178
+ }
179
+
176
180
  const jobId = createJob();
177
181
 
178
182
  setImmediate(async () => {
@@ -225,6 +229,96 @@ async function tddAuditPlugin(fastify, opts) {
225
229
  return reply.code(202).send({ jobId });
226
230
  });
227
231
 
232
+ // ── POST /audit/ai — LLM-powered agentic audit ────────────────────────────
233
+ // Accepts { path?, provider?, apiKey?, model?, baseUrl?, scanOnly?, allowWrites? }
234
+ // Falls back to cfg values for provider/apiKey/model/baseUrl when not supplied.
235
+ // Returns 202 { jobId }. Poll GET /jobs/:id or stream GET /jobs/:id/stream.
236
+ fastify.post('/audit/ai', async (request, reply) => {
237
+ const body = request.body || {};
238
+ const {
239
+ path: rawPath,
240
+ provider: bodyProvider,
241
+ apiKey: bodyApiKey,
242
+ model: bodyModel,
243
+ baseUrl: bodyBaseUrl,
244
+ depth = 'tier-1',
245
+ // scanOnly / allowWrites may still be overridden explicitly; depth takes precedence otherwise
246
+ scanOnly = null,
247
+ allowWrites = false,
248
+ // Pre-identified findings from a prior tier-3 report (triggers targeted-apply mode when depth=tier-4)
249
+ findings = null,
250
+ } = body;
251
+
252
+ const provider = bodyProvider || cfg.provider;
253
+ const apiKey = bodyApiKey || cfg.apiKey;
254
+ const model = bodyModel || cfg.model;
255
+ const baseUrl = bodyBaseUrl || cfg.baseUrl;
256
+
257
+ if (!provider || !apiKey) {
258
+ return reply.code(400).send({
259
+ error: 'provider and apiKey are required (supply in body or configure in .tdd-audit.json)',
260
+ });
261
+ }
262
+
263
+ let scanPath = process.cwd();
264
+ if (rawPath) {
265
+ try { scanPath = safeScanPath(rawPath); }
266
+ catch (e) { return reply.code(400).send({ error: e.message }); }
267
+ }
268
+
269
+ const jobId = createJob();
270
+ updateJob(jobId, { depth }); // stamp depth on initial pending state
271
+
272
+ setImmediate(async () => {
273
+ try {
274
+ const log = [];
275
+
276
+ updateJob(jobId, { status: 'running', depth, startedAt: new Date().toISOString() });
277
+
278
+ const { runAudit } = require('./auditor');
279
+ let capturedJson = null;
280
+
281
+ await runAudit({
282
+ projectDir: scanPath,
283
+ packageDir: path.join(__dirname, '..'),
284
+ provider,
285
+ apiKey,
286
+ model,
287
+ baseUrl,
288
+ outputFormat: 'json',
289
+ depth,
290
+ scanOnly,
291
+ allowWrites,
292
+ findings,
293
+ onText: (text) => {
294
+ log.push(text);
295
+ updateJob(jobId, { status: 'running', log: log.join(''), startedAt: new Date().toISOString() });
296
+ },
297
+ outputWriter: (jsonStr) => { capturedJson = jsonStr; },
298
+ });
299
+
300
+ let result;
301
+ try {
302
+ result = capturedJson ? JSON.parse(capturedJson.trim()) : { log: log.join('') };
303
+ } catch {
304
+ result = { raw: capturedJson, log: log.join('') };
305
+ }
306
+
307
+ updateJob(jobId, {
308
+ status: 'done',
309
+ completedAt: new Date().toISOString(),
310
+ result,
311
+ });
312
+ } catch (err) {
313
+ updateJob(jobId, { status: 'error', error: err.message });
314
+ }
315
+ });
316
+
317
+ reply.header('Location', `/jobs/${jobId}`);
318
+ reply.header('Retry-After', '5');
319
+ return reply.code(202).send({ jobId });
320
+ });
321
+
228
322
  // ── GET /jobs/:id ────────────────────────────────────────────────────────
229
323
  fastify.get('/jobs/:id', async (request, reply) => {
230
324
  const job = jobs.get(request.params.id);
@@ -303,6 +397,7 @@ module.exports = {
303
397
  buildApp,
304
398
  authenticate,
305
399
  safeScanPath,
400
+ assertSafeWebhook,
306
401
  createRateLimit,
307
402
  RATE_LIMIT_MAX,
308
403
  };
package/lib/reporter.js CHANGED
@@ -8,16 +8,17 @@ const { version } = require('../package.json');
8
8
  * Return findings as a structured JSON-serialisable object.
9
9
  * @param {Array} findings
10
10
  * @param {string[]} [exempted=[]]
11
+ * @param {object} [config={}] - loaded config; security_officer stamped when set
11
12
  * @returns {object}
12
13
  */
13
- function toJson(findings, exempted = []) {
14
+ function toJson(findings, exempted = [], config = {}) {
14
15
  const real = findings.filter(f => !f.likelyFalsePositive);
15
16
  const noisy = findings.filter(f => f.likelyFalsePositive);
16
17
 
17
18
  const summary = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
18
19
  for (const f of real) summary[f.severity] = (summary[f.severity] || 0) + 1;
19
20
 
20
- return {
21
+ const envelope = {
21
22
  version,
22
23
  summary,
23
24
  findings: real,
@@ -25,6 +26,9 @@ function toJson(findings, exempted = []) {
25
26
  exempted,
26
27
  scannedAt: new Date().toISOString(),
27
28
  };
29
+ if (config.security_name) envelope.security_name = config.security_name;
30
+ if (config.security_email) envelope.security_email = config.security_email;
31
+ return envelope;
28
32
  }
29
33
 
30
34
  // ─── SARIF ────────────────────────────────────────────────────────────────────
@@ -60,13 +64,27 @@ const CWE_MAP = {
60
64
  'Timing-Unsafe Comparison': 'CWE-208',
61
65
  };
62
66
 
67
+ const NPM_URL = 'https://www.npmjs.com/package/@lhi/tdd-audit';
68
+
69
+ function safeSiteUrl(raw) {
70
+ if (!raw || !raw.trim()) return NPM_URL;
71
+ try {
72
+ const parsed = new URL(raw.trim());
73
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return NPM_URL;
74
+ return raw.trim();
75
+ } catch {
76
+ return NPM_URL;
77
+ }
78
+ }
79
+
63
80
  /**
64
81
  * Return findings as a SARIF 2.1.0 object (GitHub code scanning compatible).
65
82
  * @param {Array} findings
66
83
  * @param {string} [projectDir=''] - used to build relative artifact URIs
84
+ * @param {object} [config={}] - tdd-audit config (tdd_site, badge_label, org)
67
85
  * @returns {object}
68
86
  */
69
- function toSarif(findings, projectDir = '') {
87
+ function toSarif(findings, projectDir = '', config = {}) {
70
88
  const rules = [];
71
89
  const ruleIndex = {};
72
90
 
@@ -108,9 +126,9 @@ function toSarif(findings, projectDir = '') {
108
126
  runs: [{
109
127
  tool: {
110
128
  driver: {
111
- name: '@lhi/tdd-audit',
129
+ name: config.badge_label || '@lhi/tdd-audit',
112
130
  version,
113
- informationUri: 'https://www.npmjs.com/package/@lhi/tdd-audit',
131
+ informationUri: safeSiteUrl(config.tdd_site),
114
132
  rules,
115
133
  },
116
134
  },
package/lib/scanner.js CHANGED
@@ -64,6 +64,35 @@ const VULN_PATTERNS = [
64
64
  { name: 'NEXT_PUBLIC Secret', severity: 'HIGH', skipInTests: true, pattern: /\bNEXT_PUBLIC_\w*(?:SECRET|PRIVATE|API_KEY|TOKEN|PASSWORD|CREDENTIAL)\w*/i },
65
65
  { name: 'Electron contextIsolation Off', severity: 'HIGH', pattern: /\bcontextIsolation\s*:\s*false\b/ },
66
66
  { name: 'Trojan Source', severity: 'HIGH', pattern: /[\u202A-\u202E\u2066-\u2069]/ },
67
+
68
+ // ── AI/LLM — deeper coverage (Semgrep ai-best-practices + OWASP LLM Top 10) ─
69
+ { name: 'Hardcoded Gemini Key', severity: 'CRITICAL', skipInTests: true, pattern: /['"]AIza[A-Za-z0-9_\-]{35}['"]/ },
70
+ { name: 'Hardcoded Cohere Key', severity: 'CRITICAL', skipInTests: true, pattern: /['"][A-Za-z0-9]{40}['"]\s*[,\n].*cohere|cohere.*['"][A-Za-z0-9]{40}['"]/i },
71
+ { name: 'Hardcoded Mistral Key', severity: 'CRITICAL', skipInTests: true, pattern: /['"][A-Za-z0-9]{32}['"]\s*[,\n].*mistral|mistral.*['"][A-Za-z0-9]{32}['"]/i },
72
+ { name: 'LLM Output to exec', severity: 'CRITICAL', pattern: /(?:exec|execSync|spawn|spawnSync)\s*\([^)]*(?:response|result|output|completion|generated|llmResult|aiResult)\b/i },
73
+ { name: 'Missing max_tokens', severity: 'HIGH', pattern: /(?:messages\.create|chat\.completions\.create|generateContent)\s*\(\s*\{(?![^}]*max_tokens)(?![^}]*maxTokens)/i },
74
+ { name: 'Missing system message', severity: 'MEDIUM', pattern: /(?:messages\.create|chat\.completions\.create)\s*\(\s*\{[^}]*messages\s*:\s*\[[^\]]*\{[^\]]*role\s*:\s*['"]user['"]/i },
75
+ { name: 'MCP Credential in Response', severity: 'HIGH', pattern: /(?:tool_result|toolResult|function_result)\s*[=:][^;\n]{0,200}(?:password|secret|token|api.?key|credential)/i },
76
+ { name: 'Agent Unbounded Loop', severity: 'HIGH', pattern: /while\s*\(\s*true\s*\)[^}]*(?:tool_use|function_call|tool_calls|runAgent|agent\.run)/i },
77
+ { name: 'Unsafe Model Load', severity: 'HIGH', pattern: /torch\.load\s*\([^)]*(?<!weights_only\s*=\s*True)|pickle\.load\s*\([^)]*(?:req\.|url\.|download)/i },
78
+
79
+ // ── Node.js advanced (njsscan + Bearer + ESLint security) ────────────────────
80
+ { name: 'Host Header Injection', severity: 'HIGH', pattern: /req\.(?:headers\[['"]host['"]\]|hostname|get\s*\(\s*['"]host['"]\))[^;\n]{0,120}(?:redirect|resetLink|confirmUrl|href|url)/i },
81
+ { name: 'Headless Browser SSRF', severity: 'CRITICAL', pattern: /(?:page\.goto|page\.navigate|browser\.goto|wkhtmltopdf|wkhtmltoimage|phantom\.create)\s*\([^)]*req\.(?:query|body|params)/i },
82
+ { name: 'Body Parser DoS', severity: 'HIGH', pattern: /express\.(?:json|urlencoded|text|raw)\s*\(\s*\)(?!\s*\/\/)|bodyParser\.(?:json|urlencoded)\s*\(\s*\)(?!\s*\/\/)/ },
83
+ { name: 'vm2 Deprecated', severity: 'CRITICAL', pattern: /require\s*\(\s*['"]vm2['"]\)|from\s*['"]vm2['"]/i },
84
+ { name: 'Pug Raw Output', severity: 'HIGH', pattern: /!\{(?!\s*#\{)[^}]+\}/ },
85
+ { name: 'EJS Unescaped Output', severity: 'HIGH', pattern: /<%[-=](?!=)/ },
86
+ { name: 'Handlebars Triple-Stache', severity: 'HIGH', pattern: /\{\{\{(?!\s*>)[^}]+\}\}\}/ },
87
+ { name: 'postMessage No Origin', severity: 'HIGH', pattern: /addEventListener\s*\(\s*['"]message['"]\s*,[^)]+\)(?![^{]*event\.origin|[^{]*e\.origin)/i },
88
+ { name: 'Dynamic Import User Input', severity: 'HIGH', pattern: /import\s*\(\s*req\.|import\s*\(\s*[a-z_]*[Pp]ath\s*\+|import\s*\(\s*`[^`]*\$\{req\./i },
89
+ { name: 'JWT No Revocation', severity: 'HIGH', pattern: /jwt\.sign\s*\([^)]*expiresIn\s*:\s*['"][0-9]+[dDhH](?:[0-9]+)?['"]/i },
90
+ { name: 'X-Powered-By Exposed', severity: 'MEDIUM', pattern: /app\s*=\s*express\s*\(\s*\)(?![^;]{0,500}disable\s*\(\s*['"]x-powered-by['"])/i },
91
+ { name: 'GraphQL Introspection On', severity: 'HIGH', pattern: /introspection\s*:\s*true\b/i },
92
+ { name: 'GraphQL No Depth Limit', severity: 'MEDIUM', pattern: /new ApolloServer\s*\(\s*\{(?![^}]*depthLimit|[^}]*createDepthLimitPlugin)/i },
93
+ { name: 'Sequelize TLS Disabled', severity: 'HIGH', pattern: /dialectOptions[^}]*ssl\s*:\s*(?:false|require\s*:\s*false)/i },
94
+ { name: 'Silent Exception Swallow', severity: 'MEDIUM', skipInTests: true, pattern: /catch\s*\([^)]*\)\s*\{\s*(?:\/\/[^\n]*)?\s*\}/i },
95
+ { name: 'Insecure WebSocket URL', severity: 'MEDIUM', skipInTests: true, pattern: /new WebSocket\s*\(\s*['"]ws:\/\/(?!localhost|127\.0\.0\.1)/i },
67
96
  ];
68
97
 
69
98
  const SCAN_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.go', '.dart', '.yml', '.yaml']);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lhi/tdd-audit",
3
- "version": "1.16.0",
3
+ "version": "1.20.0",
4
4
  "description": "Security skill installer for Claude Code, Gemini CLI, Cursor, Codex, and OpenCode. Patches vulnerabilities using a Red-Green-Refactor exploit-test protocol.",
5
5
  "main": "index.js",
6
6
  "bin": {