@wrongstack/plugins 0.295.0 → 0.295.1

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.
Files changed (90) hide show
  1. package/dist/accessibility-auditor/index.d.ts.map +1 -1
  2. package/dist/accessibility-auditor.js +19 -2
  3. package/dist/agent-handoff/index.d.ts.map +1 -1
  4. package/dist/agent-handoff.js +30 -1
  5. package/dist/auto-doc/index.d.ts.map +1 -1
  6. package/dist/auto-doc.js +3 -1
  7. package/dist/auto-i18n-extractor.js +5 -0
  8. package/dist/branch-guard/index.d.ts.map +1 -1
  9. package/dist/branch-guard.js +103 -1
  10. package/dist/changelog-writer/index.d.ts.map +1 -1
  11. package/dist/changelog-writer.js +137 -3
  12. package/dist/checkpoint/index.d.ts +2 -0
  13. package/dist/checkpoint/index.d.ts.map +1 -1
  14. package/dist/checkpoint.js +34 -8
  15. package/dist/code-metrics.js +5 -0
  16. package/dist/commit-validator/index.d.ts.map +1 -1
  17. package/dist/commit-validator.js +17 -7
  18. package/dist/context-pins/index.d.ts.map +1 -1
  19. package/dist/context-pins.js +6 -5
  20. package/dist/cost-tracker/index.d.ts.map +1 -1
  21. package/dist/cost-tracker.js +101 -1
  22. package/dist/cron/index.d.ts.map +1 -1
  23. package/dist/cron.js +1 -0
  24. package/dist/dead-code-detector.js +5 -0
  25. package/dist/dependency-vulnerability-gate/index.d.ts.map +1 -1
  26. package/dist/dependency-vulnerability-gate.js +43 -8
  27. package/dist/diff-summary/index.d.ts.map +1 -1
  28. package/dist/diff-summary.js +100 -1
  29. package/dist/doc-sync-guard.js +5 -0
  30. package/dist/duplicate-code-detector/index.d.ts.map +1 -1
  31. package/dist/duplicate-code-detector.js +102 -2
  32. package/dist/feature-flag-tracker/index.d.ts.map +1 -1
  33. package/dist/feature-flag-tracker.js +30 -4
  34. package/dist/file-watcher/index.d.ts.map +1 -1
  35. package/dist/file-watcher.js +6 -7
  36. package/dist/format-on-save/index.d.ts +2 -0
  37. package/dist/format-on-save/index.d.ts.map +1 -1
  38. package/dist/format-on-save.js +288 -59
  39. package/dist/import-organizer/index.d.ts.map +1 -1
  40. package/dist/import-organizer.js +140 -20
  41. package/dist/index.js +1265 -544
  42. package/dist/interface-contract-guard/index.d.ts.map +1 -1
  43. package/dist/interface-contract-guard.js +60 -26
  44. package/dist/knowledge-graph/index.d.ts.map +1 -1
  45. package/dist/knowledge-graph.js +7 -6
  46. package/dist/lint-gate/index.d.ts.map +1 -1
  47. package/dist/lint-gate.js +103 -1
  48. package/dist/loop-breaker/index.d.ts.map +1 -1
  49. package/dist/loop-breaker.js +15 -5
  50. package/dist/migration-planner.js +3 -0
  51. package/dist/notify-hub/index.d.ts.map +1 -1
  52. package/dist/notify-hub.js +41 -4
  53. package/dist/prompt-firewall/index.d.ts.map +1 -1
  54. package/dist/prompt-firewall.js +140 -11
  55. package/dist/refactor-suggester/index.d.ts.map +1 -1
  56. package/dist/refactor-suggester.js +119 -4
  57. package/dist/runtime/bounded-map.d.ts +86 -0
  58. package/dist/runtime/bounded-map.d.ts.map +1 -0
  59. package/dist/runtime/credential-patterns.d.ts +42 -0
  60. package/dist/runtime/credential-patterns.d.ts.map +1 -0
  61. package/dist/runtime/index.d.ts +3 -0
  62. package/dist/runtime/index.d.ts.map +1 -1
  63. package/dist/runtime/local-bin.d.ts +120 -0
  64. package/dist/runtime/local-bin.d.ts.map +1 -0
  65. package/dist/runtime/safe-json.d.ts +25 -0
  66. package/dist/runtime/safe-json.d.ts.map +1 -0
  67. package/dist/runtime.js +291 -22
  68. package/dist/schema-evolution-guard.js +5 -0
  69. package/dist/secret-scanner/index.d.ts.map +1 -1
  70. package/dist/secret-scanner.js +175 -48
  71. package/dist/security-hotspot-scanner/index.d.ts.map +1 -1
  72. package/dist/security-hotspot-scanner.js +132 -1
  73. package/dist/semantic-search-indexer/index.d.ts.map +1 -1
  74. package/dist/semantic-search-indexer.js +7 -1
  75. package/dist/semver-bump/index.d.ts.map +1 -1
  76. package/dist/semver-bump.js +21 -2
  77. package/dist/template-engine/index.d.ts.map +1 -1
  78. package/dist/template-engine.js +30 -2
  79. package/dist/test-coverage-gate.js +5 -0
  80. package/dist/test-runner-gate/index.d.ts.map +1 -1
  81. package/dist/test-runner-gate.js +130 -8
  82. package/dist/todo-listener/index.d.ts.map +1 -1
  83. package/dist/todo-listener.js +30 -1
  84. package/dist/todo-tracker/index.d.ts.map +1 -1
  85. package/dist/todo-tracker.js +3 -4
  86. package/dist/token-throttle/index.d.ts.map +1 -1
  87. package/dist/token-throttle.js +17 -2
  88. package/dist/type-gate/index.d.ts.map +1 -1
  89. package/dist/type-gate.js +96 -16
  90. package/package.json +3 -3
@@ -1,20 +1,44 @@
1
- // src/secret-scanner/index.ts
2
- var BASE_PATTERNS = [
1
+ // src/runtime/credential-patterns.ts
2
+ var CREDENTIAL_PATTERNS = [
3
3
  // LLM provider keys
4
4
  {
5
5
  type: "anthropic_key",
6
6
  regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g
7
7
  },
8
- { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
9
- // GitHub
8
+ { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?!ant)(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
9
+ // GitHub. `ghp_` is only the personal-access-token prefix — the OAuth
10
+ // (`gho_`), user-to-server (`ghu_`), server-to-server (`ghs_`) and
11
+ // refresh (`ghr_`) tokens grant the same or broader access and were
12
+ // previously not detected at all.
10
13
  { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
14
+ {
15
+ type: "github_oauth_token",
16
+ regex: /(?<![A-Za-z0-9])gh[ousr]_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g
17
+ },
11
18
  { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
19
+ // GitLab
20
+ { type: "gitlab_pat", regex: /(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
21
+ {
22
+ type: "gitlab_runner_token",
23
+ regex: /(?<![A-Za-z0-9])glrt-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
24
+ },
25
+ // npm — a leaked publish token is a supply-chain compromise.
26
+ { type: "npm_token", regex: /(?<![A-Za-z0-9])npm_[A-Za-z0-9]{36}(?![A-Za-z0-9])/g },
12
27
  // AWS
13
28
  { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
14
29
  // GCP
15
30
  { type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
16
- // Slack
17
- { type: "slack_token", regex: /(?<![A-Za-z0-9-])xox[abpos]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
31
+ // Slack. `xoxe` (token-rotation) and `xapp` (app-level) were missing;
32
+ // both are as sensitive as the bot/user tokens already covered.
33
+ {
34
+ type: "slack_token",
35
+ regex: /(?<![A-Za-z0-9-])xox[abposer]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g
36
+ },
37
+ { type: "slack_app_token", regex: /(?<![A-Za-z0-9-])xapp-\d-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
38
+ {
39
+ type: "slack_webhook",
40
+ regex: /https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9_-]+\/B[A-Za-z0-9_-]+\/[A-Za-z0-9]{16,}/g
41
+ },
18
42
  // Stripe
19
43
  {
20
44
  type: "stripe_key",
@@ -25,7 +49,7 @@ var BASE_PATTERNS = [
25
49
  // Telegram
26
50
  {
27
51
  type: "telegram_bot_token",
28
- regex: /\/bot\d+:[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
52
+ regex: /(?:(?<![A-Za-z0-9_])|(?<=(?:^|[^A-Za-z0-9_])bot))\d+:[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
29
53
  },
30
54
  // JWT
31
55
  {
@@ -35,35 +59,93 @@ var BASE_PATTERNS = [
35
59
  // Private keys
36
60
  {
37
61
  type: "private_key",
38
- regex: /(?:^|\n)-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----[\s\S]*?-----END (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----(?!\S)/g
62
+ regex: /(?:^|\n)(?:-----BEGIN (?:RSA|EC|OPENSSH|DSA)? ?PRIVATE KEY-----[\s\S]*?-----END (?:RSA|EC|OPENSSH|DSA)? ?PRIVATE KEY-----|-----BEGIN PGP PRIVATE KEY BLOCK-----[\s\S]*?-----END PGP PRIVATE KEY BLOCK-----)(?!\S)/g
39
63
  },
40
64
  // AI/ML provider tokens
41
65
  { type: "huggingface_token", regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g },
42
66
  { type: "replicate_token", regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
43
67
  { type: "perplexity_key", regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
44
68
  { type: "groq_key", regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
69
+ // SaaS / infrastructure tokens — each grants API access on the user's
70
+ // account, and each was previously invisible to this gate.
71
+ { type: "sendgrid_key", regex: /(?<![A-Za-z0-9])SG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
72
+ { type: "digitalocean_token", regex: /(?<![A-Za-z0-9])dop_v1_[a-f0-9]{64}(?![A-Za-z0-9])/g },
73
+ { type: "doppler_token", regex: /(?<![A-Za-z0-9])dp\.(?:pt|st|sa|scim|audit)\.[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
74
+ { type: "shopify_token", regex: /(?<![A-Za-z0-9])shp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}(?![A-Za-z0-9])/g },
75
+ { type: "docker_pat", regex: /(?<![A-Za-z0-9])dckr_pat_[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
76
+ { type: "linear_key", regex: /(?<![A-Za-z0-9])lin_api_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
77
+ { type: "atlassian_token", regex: /(?<![A-Za-z0-9])ATATT3[A-Za-z0-9_\-=]{40,}(?![A-Za-z0-9_\-=])/g },
78
+ { type: "square_token", regex: /(?<![A-Za-z0-9])(?:sq0(?:atp|csp)-|EAAA)[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
79
+ {
80
+ type: "azure_storage_key",
81
+ regex: /AccountKey=[A-Za-z0-9+/]{80,}={0,2}/g
82
+ },
83
+ {
84
+ type: "google_oauth_client_secret",
85
+ regex: /(?<![A-Za-z0-9_-])GOCSPX-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
86
+ },
45
87
  // Bearer tokens
46
88
  {
47
89
  type: "bearer_token",
48
- regex: /(?:^|[^A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{12,512}=*(?![A-Za-z0-9._~+/-])/g
90
+ regex: /(?<![A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{12,512}=*(?![A-Za-z0-9._~+/-])/g
49
91
  },
50
- // Database URIs
51
- { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s"'`]+/g },
52
- { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
53
- { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
54
- { type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g }
92
+ // Database URIs. Require password-bearing user-info; credential-free values stay scannable.
93
+ { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g },
94
+ {
95
+ type: "postgres_uri",
96
+ // Query parsers decode percent-encoded parameter names. Recognize each
97
+ // encoded character in `password` so mixed forms such as `pass%77ord`
98
+ // cannot bypass detection while keeping the scan strictly bounded.
99
+ regex: /postgres(?:ql)?:\/\/(?:[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+|[^&\s?"'`#]{1,2048}\?(?:(?!(?:p|%70)(?:a|%61)(?:s|%73)(?:s|%73)(?:w|%77)(?:o|%6[fF])(?:r|%72)(?:d|%64)=)[^&\s#"'`]{1,256}&){0,32}(?:p|%70)(?:a|%61)(?:s|%73)(?:s|%73)(?:w|%77)(?:o|%6[fF])(?:r|%72)(?:d|%64)=[^&\s#"'`]{1,4096})/g
100
+ },
101
+ { type: "mysql_uri", regex: /mysql:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g },
102
+ { type: "redis_uri", regex: /redis:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g }
55
103
  ];
104
+ function cloneCredentialPatterns() {
105
+ return CREDENTIAL_PATTERNS.map((p) => ({
106
+ type: p.type,
107
+ regex: new RegExp(p.regex.source, p.regex.flags)
108
+ }));
109
+ }
110
+
111
+ // src/secret-scanner/index.ts
112
+ var BASE_PATTERNS = cloneCredentialPatterns();
56
113
  var PATTERNS = [...BASE_PATTERNS];
57
114
  var patternCacheKey = "";
115
+ var GROUP_INDEX_OF_PATTERN = [];
58
116
  var COMBINED_REGEX = buildCombinedRegex(PATTERNS);
59
- var RE_DOS_MAX_SCAN_LENGTH = 1e5;
117
+ var SCAN_WINDOW_LENGTH = 1e5;
118
+ var SCAN_WINDOW_OVERLAP = 4096;
119
+ var MAX_TOTAL_SCAN_LENGTH = 8 * 1024 * 1024;
120
+ var TOO_LARGE_MARKER = "unscannable_oversized_input";
60
121
  var RE_DOS_TIMEOUT_MS = 100;
122
+ function countCaptureGroups(source) {
123
+ try {
124
+ return new RegExp(`${source}|`).exec("").length - 1;
125
+ } catch {
126
+ return 0;
127
+ }
128
+ }
129
+ function patternTypeForGroups(groups) {
130
+ for (let i = 0; i < PATTERNS.length; i++) {
131
+ const at = GROUP_INDEX_OF_PATTERN[i];
132
+ if (at !== void 0 && groups[at] !== void 0) return PATTERNS[i].type;
133
+ }
134
+ return void 0;
135
+ }
61
136
  function buildCombinedRegex(patterns) {
62
- const newCacheKey = JSON.stringify(patterns.map((p) => p.type));
137
+ const newCacheKey = JSON.stringify(patterns.map((p) => [p.type, p.regex.source]));
63
138
  if (newCacheKey === patternCacheKey && COMBINED_REGEX) {
64
139
  return COMBINED_REGEX;
65
140
  }
66
141
  patternCacheKey = newCacheKey;
142
+ const offsets = [];
143
+ let cursor = 0;
144
+ for (const p of patterns) {
145
+ offsets.push(cursor);
146
+ cursor += 1 + countCaptureGroups(p.regex.source);
147
+ }
148
+ GROUP_INDEX_OF_PATTERN = offsets;
67
149
  return new RegExp(patterns.map((p) => `(${p.regex.source})`).join("|"), "g");
68
150
  }
69
151
  var state = {
@@ -81,29 +163,37 @@ var state = {
81
163
  /** PostToolUse hook handle so teardown can unregister. */
82
164
  postHookUnregister: null
83
165
  };
84
- function findMatches(text) {
85
- if (!text) return [];
86
- if (text.length > RE_DOS_MAX_SCAN_LENGTH) return [];
87
- const found = /* @__PURE__ */ new Set();
166
+ function scanWindow(window, found, startTime) {
88
167
  COMBINED_REGEX.lastIndex = 0;
89
168
  let m;
90
- const startTime = performance.now();
91
- while ((m = COMBINED_REGEX.exec(text)) !== null) {
169
+ while ((m = COMBINED_REGEX.exec(window)) !== null) {
92
170
  if (performance.now() - startTime > RE_DOS_TIMEOUT_MS) {
93
171
  throw new Error(
94
- `secret-scanner: ReDoS timeout \u2014 regex scan exceeded ${RE_DOS_TIMEOUT_MS}ms on ${text.length}-char input`
172
+ `secret-scanner: ReDoS timeout \u2014 regex scan exceeded ${RE_DOS_TIMEOUT_MS}ms on ${window.length}-char window`
95
173
  );
96
174
  }
97
- for (let i = 0; i < PATTERNS.length; i++) {
98
- if (m[i + 1] !== void 0) {
99
- found.add(PATTERNS[i].type);
100
- break;
101
- }
102
- }
175
+ const type = patternTypeForGroups(m.slice(1));
176
+ if (type !== void 0) found.add(type);
103
177
  if (m.index === COMBINED_REGEX.lastIndex) {
104
178
  COMBINED_REGEX.lastIndex += 1;
105
179
  }
106
180
  }
181
+ }
182
+ function findMatches(text) {
183
+ if (!text) return [];
184
+ const found = /* @__PURE__ */ new Set();
185
+ const startTime = performance.now();
186
+ if (text.length <= SCAN_WINDOW_LENGTH) {
187
+ scanWindow(text, found, startTime);
188
+ return Array.from(found).sort();
189
+ }
190
+ if (text.length > MAX_TOTAL_SCAN_LENGTH) {
191
+ return [TOO_LARGE_MARKER];
192
+ }
193
+ const stride = SCAN_WINDOW_LENGTH - SCAN_WINDOW_OVERLAP;
194
+ for (let offset = 0; offset < text.length; offset += stride) {
195
+ scanWindow(text.slice(offset, offset + SCAN_WINDOW_LENGTH), found, startTime);
196
+ }
107
197
  return Array.from(found).sort();
108
198
  }
109
199
  function scanInput(input, depth = 0) {
@@ -129,30 +219,55 @@ function scanInput(input, depth = 0) {
129
219
  }
130
220
  return null;
131
221
  }
222
+ function redactString(text) {
223
+ if (text.length > SCAN_WINDOW_LENGTH) {
224
+ return { ok: false, reason: "oversized_input" };
225
+ }
226
+ const startTime = performance.now();
227
+ let cursor = 0;
228
+ let output = "";
229
+ COMBINED_REGEX.lastIndex = 0;
230
+ let match;
231
+ while ((match = COMBINED_REGEX.exec(text)) !== null) {
232
+ if (performance.now() - startTime > RE_DOS_TIMEOUT_MS) {
233
+ return { ok: false, reason: "oversized_input" };
234
+ }
235
+ const type = patternTypeForGroups(match.slice(1));
236
+ output += text.slice(cursor, match.index);
237
+ output += `[REDACTED:${type ?? "unknown"}]`;
238
+ cursor = COMBINED_REGEX.lastIndex;
239
+ if (match.index === COMBINED_REGEX.lastIndex) {
240
+ COMBINED_REGEX.lastIndex += 1;
241
+ }
242
+ }
243
+ if (cursor === 0) return { ok: true, value: text };
244
+ return { ok: true, value: output + text.slice(cursor) };
245
+ }
132
246
  function redactInput(input, depth = 0) {
133
- if (input === null || input === void 0) return input;
134
- if (depth > 50) return input;
247
+ if (input === null || input === void 0) return { ok: true, value: input };
248
+ if (depth > 50) return { ok: false, reason: "maximum_depth_exceeded" };
135
249
  if (typeof input === "string") {
136
- return input.replace(COMBINED_REGEX, (_match, ...groups) => {
137
- for (let i = 0; i < PATTERNS.length; i++) {
138
- if (groups[i] !== void 0) {
139
- return `[REDACTED:${PATTERNS[i].type}]`;
140
- }
141
- }
142
- return "[REDACTED:unknown]";
143
- });
250
+ return redactString(input);
144
251
  }
145
252
  if (Array.isArray(input)) {
146
- return input.map((item) => redactInput(item, depth + 1));
253
+ const out = [];
254
+ for (const item of input) {
255
+ const redacted = redactInput(item, depth + 1);
256
+ if (!redacted.ok) return redacted;
257
+ out.push(redacted.value);
258
+ }
259
+ return { ok: true, value: out };
147
260
  }
148
261
  if (typeof input === "object") {
149
262
  const out = {};
150
263
  for (const [k, v] of Object.entries(input)) {
151
- out[k] = redactInput(v, depth + 1);
264
+ const redacted = redactInput(v, depth + 1);
265
+ if (!redacted.ok) return redacted;
266
+ out[k] = redacted.value;
152
267
  }
153
- return out;
268
+ return { ok: true, value: out };
154
269
  }
155
- return input;
270
+ return { ok: true, value: input };
156
271
  }
157
272
  var DEFAULTS = {
158
273
  matcher: "bash|write|edit",
@@ -212,20 +327,21 @@ function buildHook(cfg, log) {
212
327
  }
213
328
  if (cfg.mode === "redact") {
214
329
  const redacted = redactInput(input.toolInput);
215
- if (redacted !== null && typeof redacted === "object" && !Array.isArray(redacted)) {
330
+ if (redacted.ok && redacted.value !== null && typeof redacted.value === "object" && !Array.isArray(redacted.value)) {
216
331
  state.redactCount += 1;
217
332
  log.info(`[secret-scanner] redacted ${toolName} \u2014 matched: ${summary}`);
218
333
  return {
219
334
  decision: "allow",
220
- modifiedInput: redacted,
335
+ modifiedInput: redacted.value,
221
336
  additionalContext: `secret-scanner: redacted ${matched.length} credential pattern(s) from the ${toolName} arguments before execution.`
222
337
  };
223
338
  }
224
339
  state.blockCount += 1;
225
340
  state.lastBlock = { toolName, matchedTypes: matched, when };
341
+ const detail = !redacted.ok ? redacted.reason === "oversized_input" ? "an input field exceeds the safe scan limit" : "the input exceeds the safe nesting depth" : "the input has a non-object shape";
226
342
  return {
227
343
  decision: "block",
228
- reason: `secret-scanner: cannot safely redact '${toolName}' input (non-object shape); refusing to run.`
344
+ reason: `secret-scanner: cannot safely redact '${toolName}' because ${detail}; refusing to run.`
229
345
  };
230
346
  }
231
347
  state.allowCount += 1;
@@ -243,10 +359,21 @@ function buildPostHook(cfg, log) {
243
359
  const matched = findMatches(result.content);
244
360
  if (matched.length === 0) return;
245
361
  const toolName = input.toolName ?? "unknown";
246
- const summary = matched.join(", ");
362
+ const oversized = matched.includes(TOO_LARGE_MARKER);
363
+ const credentialMatches = matched.filter((type) => type !== TOO_LARGE_MARKER);
364
+ if (oversized && credentialMatches.length === 0) {
365
+ log.warn(
366
+ `[secret-scanner] POST-TOOL UNSCANNABLE: ${toolName} output exceeds ${MAX_TOTAL_SCAN_LENGTH} characters`
367
+ );
368
+ return {
369
+ additionalContext: `
370
+ \u26A0\uFE0F secret-scanner: the output of '${toolName}' exceeds the safe scan limit and could not be inspected for plaintext credentials. Do not treat this output as verified clean; avoid echoing, storing, committing, or transmitting it until it can be reviewed in smaller bounded sections.`
371
+ };
372
+ }
373
+ const summary = credentialMatches.join(", ");
247
374
  const when = (/* @__PURE__ */ new Date()).toISOString();
248
375
  state.leakCount += 1;
249
- state.lastLeak = { toolName, matchedTypes: matched, when };
376
+ state.lastLeak = { toolName, matchedTypes: credentialMatches, when };
250
377
  log.warn(`[secret-scanner] POST-TOOL LEAK: ${toolName} output matched ${summary}`);
251
378
  return {
252
379
  additionalContext: `
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/security-hotspot-scanner/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AA6QrD,QAAA,MAAM,MAAM,EAAE,MAkQb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/security-hotspot-scanner/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAkRrD,QAAA,MAAM,MAAM,EAAE,MAkQb,CAAC;eAEa,MAAM"}
@@ -4,6 +4,137 @@ import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2 }
4
4
 
5
5
  // src/runtime/index.ts
6
6
  import { basename, extname, isAbsolute, relative, resolve } from "node:path";
7
+
8
+ // src/runtime/local-bin.ts
9
+ import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
10
+
11
+ // src/runtime/bounded-map.ts
12
+ var BoundedMap = class {
13
+ map = /* @__PURE__ */ new Map();
14
+ max;
15
+ ttlMs;
16
+ now;
17
+ /** Entries dropped to stay under `max`. Surfaced by plugin health(). */
18
+ evictions = 0;
19
+ constructor(options) {
20
+ const normalizedMax = Math.floor(options.max);
21
+ this.max = Number.isSafeInteger(normalizedMax) ? Math.max(1, normalizedMax) : 1;
22
+ this.ttlMs = options.ttlMs;
23
+ this.now = options.now ?? Date.now;
24
+ }
25
+ expired(entry) {
26
+ return this.ttlMs !== void 0 && this.now() - entry.storedAt > this.ttlMs;
27
+ }
28
+ get(key) {
29
+ const entry = this.map.get(key);
30
+ if (entry === void 0) return void 0;
31
+ if (this.expired(entry)) {
32
+ this.map.delete(key);
33
+ return void 0;
34
+ }
35
+ this.map.delete(key);
36
+ this.map.set(key, entry);
37
+ return entry.value;
38
+ }
39
+ /**
40
+ * Read without promoting the key to most-recently-used. Use for
41
+ * diagnostics that must not perturb the eviction order.
42
+ */
43
+ peek(key) {
44
+ const entry = this.map.get(key);
45
+ if (entry === void 0 || this.expired(entry)) return void 0;
46
+ return entry.value;
47
+ }
48
+ has(key) {
49
+ const entry = this.map.get(key);
50
+ if (entry === void 0) return false;
51
+ if (this.expired(entry)) {
52
+ this.map.delete(key);
53
+ return false;
54
+ }
55
+ return true;
56
+ }
57
+ set(key, value) {
58
+ this.map.delete(key);
59
+ this.map.set(key, { value, storedAt: this.now() });
60
+ while (this.map.size > this.max) {
61
+ const coldest = this.map.keys().next().value;
62
+ if (coldest === void 0) break;
63
+ this.map.delete(coldest);
64
+ this.evictions += 1;
65
+ }
66
+ return this;
67
+ }
68
+ delete(key) {
69
+ return this.map.delete(key);
70
+ }
71
+ clear() {
72
+ this.map.clear();
73
+ this.evictions = 0;
74
+ }
75
+ get size() {
76
+ return this.map.size;
77
+ }
78
+ /** How many entries have been dropped to respect `max`, since the last clear. */
79
+ get evictionCount() {
80
+ return this.evictions;
81
+ }
82
+ /** Drop every expired entry. Cheap enough to call from a status tool. */
83
+ prune() {
84
+ if (this.ttlMs === void 0) return 0;
85
+ let removed = 0;
86
+ for (const [key, entry] of this.map) {
87
+ if (this.expired(entry)) {
88
+ this.map.delete(key);
89
+ removed += 1;
90
+ }
91
+ }
92
+ return removed;
93
+ }
94
+ /** Live (non-expired) entries, coldest first. */
95
+ *entries() {
96
+ for (const [key, entry] of this.map) {
97
+ if (!this.expired(entry)) yield [key, entry.value];
98
+ }
99
+ }
100
+ [Symbol.iterator]() {
101
+ return this.entries();
102
+ }
103
+ };
104
+ var BoundedSet = class {
105
+ inner;
106
+ constructor(options) {
107
+ this.inner = new BoundedMap(options);
108
+ }
109
+ has(value) {
110
+ return this.inner.has(value);
111
+ }
112
+ add(value) {
113
+ this.inner.set(value, true);
114
+ return this;
115
+ }
116
+ delete(value) {
117
+ return this.inner.delete(value);
118
+ }
119
+ clear() {
120
+ this.inner.clear();
121
+ }
122
+ get size() {
123
+ return this.inner.size;
124
+ }
125
+ /** How many entries have been dropped to respect `max`, since the last clear. */
126
+ get evictionCount() {
127
+ return this.inner.evictionCount;
128
+ }
129
+ *values() {
130
+ for (const [key] of this.inner) yield key;
131
+ }
132
+ [Symbol.iterator]() {
133
+ return this.values();
134
+ }
135
+ };
136
+
137
+ // src/runtime/index.ts
7
138
  var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
8
139
  function hasLeadingDash(arg) {
9
140
  return arg.length > 0 && arg.startsWith("-");
@@ -30,7 +161,7 @@ var state = {
30
161
  skippedCount: 0,
31
162
  blockedCount: 0,
32
163
  lastResult: null,
33
- knownHotspots: /* @__PURE__ */ new Set(),
164
+ knownHotspots: new BoundedSet({ max: 5e3 }),
34
165
  hookUnregister: null
35
166
  };
36
167
  var DEFAULTS = {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/semantic-search-indexer/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AA6brD,QAAA,MAAM,MAAM,EAAE,MA6Nb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/semantic-search-indexer/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAqcrD,QAAA,MAAM,MAAM,EAAE,MA6Nb,CAAC;eAEa,MAAM"}
@@ -1,7 +1,9 @@
1
1
  // src/semantic-search-indexer/index.ts
2
2
  import * as fs from "node:fs/promises";
3
3
  import { isAbsolute, relative, resolve } from "node:path";
4
+ import { DEFAULT_WALK_IGNORE_DIRS } from "@wrongstack/core/utils";
4
5
  var API_VERSION = "^0.1.10";
6
+ var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5
7
  var state = {
6
8
  index: null,
7
9
  cachedPath: null,
@@ -45,7 +47,11 @@ var DEFAULTS = {
45
47
  ".scss",
46
48
  ".html"
47
49
  ],
48
- excludePatterns: ["node_modules", "\\.git", "\\.wrongstack", "\\.temp_files", "coverage", "dist"],
50
+ excludePatterns: [
51
+ ...DEFAULT_WALK_IGNORE_DIRS.map(escapeRegex),
52
+ "\\.wrongstack",
53
+ "\\.temp_files"
54
+ ],
49
55
  maxFileBytes: 1e6,
50
56
  defaultLimit: 10,
51
57
  minTokenLength: 2,
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/semver-bump/index.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAuCrD,KAAK,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAErD,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAqGD;+DAC+D;AAC/D,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAQnF;AAmBD,wBAAgB,aAAa,CAAC,OAAO,EAAE,kBAAkB,EAAE,GAAG,QAAQ,CAIrE;AAmED,QAAA,MAAM,MAAM,EAAE,MAgcb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/semver-bump/index.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAuCrD,KAAK,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAErD,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAqGD;+DAC+D;AAC/D,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAQnF;AAmBD,wBAAgB,aAAa,CAAC,OAAO,EAAE,kBAAkB,EAAE,GAAG,QAAQ,CAIrE;AAmED,QAAA,MAAM,MAAM,EAAE,MA0db,CAAC;eAEa,MAAM"}
@@ -293,10 +293,29 @@ var plugin = {
293
293
  }
294
294
  }
295
295
  } else {
296
+ const pending = [];
296
297
  for (const manifest of changed) {
297
- const pkgData = JSON.parse(await readFile(manifest, "utf-8"));
298
+ let pkgData;
299
+ try {
300
+ pkgData = JSON.parse(await readFile(manifest, "utf-8"));
301
+ } catch (err) {
302
+ return {
303
+ ok: false,
304
+ error: `cannot bump: ${manifest} is not readable as JSON (${toErrorMessage(err)}). No manifests were modified.`
305
+ };
306
+ }
307
+ if (!pkgData || typeof pkgData !== "object") {
308
+ return {
309
+ ok: false,
310
+ error: `cannot bump: ${manifest} does not contain a JSON object. No manifests were modified.`
311
+ };
312
+ }
298
313
  pkgData.version = newVersion;
299
- await writeFile(manifest, JSON.stringify(pkgData, null, 2) + "\n", "utf-8");
314
+ pending.push({ path: manifest, contents: `${JSON.stringify(pkgData, null, 2)}
315
+ ` });
316
+ }
317
+ for (const { path, contents } of pending) {
318
+ await writeFile(path, contents, "utf-8");
300
319
  }
301
320
  }
302
321
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/template-engine/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAsHrD,QAAA,MAAM,MAAM,EAAE,MA4Sb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/template-engine/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AA+HrD,QAAA,MAAM,MAAM,EAAE,MA4Sb,CAAC;eAEa,MAAM"}
@@ -1,6 +1,31 @@
1
1
  // src/template-engine/index.ts
2
2
  import { readFile, writeFile } from "node:fs/promises";
3
- import { isAbsolute } from "node:path";
3
+ import { isAbsolute as isAbsolute2 } from "node:path";
4
+
5
+ // src/runtime/index.ts
6
+ import { basename, extname, isAbsolute, relative, resolve } from "node:path";
7
+
8
+ // src/runtime/local-bin.ts
9
+ import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
10
+
11
+ // src/runtime/index.ts
12
+ var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
13
+ function hasLeadingDash(arg) {
14
+ return arg.length > 0 && arg.startsWith("-");
15
+ }
16
+ function withinProjectPath(projectRoot, candidate) {
17
+ if (candidate.length === 0 || candidate.length > 4096) return false;
18
+ if (hasLeadingDash(candidate)) return false;
19
+ const resolved = isAbsolute(candidate) ? resolve(candidate) : resolve(projectRoot, candidate);
20
+ const rel = relative(projectRoot, resolved);
21
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
22
+ }
23
+ function withinProject(p) {
24
+ const cwd = process.cwd();
25
+ return withinProjectPath(cwd, p) || relative(cwd, p) === ".";
26
+ }
27
+
28
+ // src/template-engine/index.ts
4
29
  var API_VERSION = "^0.1.10";
5
30
  var templates = /* @__PURE__ */ new Map();
6
31
  function expandTemplate(template, variables) {
@@ -47,9 +72,12 @@ function renderTemplateRaw(template, variables) {
47
72
  return result;
48
73
  }
49
74
  function validateRelativeTemplatePath(field, value) {
50
- if (isAbsolute(value) || value.split(/[\\/]+/).includes("..")) {
75
+ if (isAbsolute2(value) || value.split(/[\\/]+/).includes("..")) {
51
76
  return `${field} must be a relative path without ".." components`;
52
77
  }
78
+ if (!withinProject(value)) {
79
+ return `${field} must resolve inside the project directory`;
80
+ }
53
81
  return null;
54
82
  }
55
83
  var plugin = {
@@ -3,6 +3,11 @@ import { readFileSync } from "node:fs";
3
3
 
4
4
  // src/runtime/index.ts
5
5
  import { basename, extname, isAbsolute, relative, resolve } from "node:path";
6
+
7
+ // src/runtime/local-bin.ts
8
+ import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
9
+
10
+ // src/runtime/index.ts
6
11
  var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
7
12
  function hasLeadingDash(arg) {
8
13
  return arg.length > 0 && arg.startsWith("-");
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test-runner-gate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAscrD,QAAA,MAAM,MAAM,EAAE,MA+Tb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test-runner-gate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAufrD,QAAA,MAAM,MAAM,EAAE,MA+Tb,CAAC;eAEa,MAAM"}