@rmyndharis/aimhooman 0.1.8 → 0.3.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/src/report.mjs CHANGED
@@ -21,6 +21,7 @@ export function human(findings, tone) {
21
21
  // while bounding stderr for repeated-rule scans.
22
22
  const shown = findings.slice(0, HUMAN_FINDING_CAP);
23
23
  const hidden = findings.length - shown.length;
24
+ const fixesPrinted = new Set();
24
25
  for (const f of shown) {
25
26
  const loc = f.path
26
27
  ? `${visible(f.path)}${f.line ? `:${f.line}` : ''}`
@@ -34,9 +35,17 @@ export function human(findings, tone) {
34
35
  }
35
36
  // Render the whole remediation array, not just the first entry. Several
36
37
  // rules carry a second line (e.g. "rotate the key if it was ever exposed")
37
- // that the previous single-index render dropped silently.
38
- for (const remedy of (f.remediation || [])) {
39
- out += ` fix: ${remedy}\n`;
38
+ // that the previous single-index render dropped silently. UT-08: print a
39
+ // rule's fix once — a repeated rule (20 hits of the same secret rule)
40
+ // used to reprint the identical fix block for every finding.
41
+ const remedies = f.remediation || [];
42
+ if (remedies.length && fixesPrinted.has(f.ruleId)) {
43
+ out += ` fix: as above for ${f.ruleId}\n`;
44
+ } else {
45
+ if (remedies.length) fixesPrinted.add(f.ruleId);
46
+ for (const remedy of remedies) {
47
+ out += ` fix: ${remedy}\n`;
48
+ }
40
49
  }
41
50
  out += '\n';
42
51
  }
@@ -68,13 +77,22 @@ export function jsonReport(findings, metadata = {}) {
68
77
  return JSON.stringify({ schema_version: 1, ...metadata, findings: safe }, null, 2);
69
78
  }
70
79
 
80
+ // Built-in secret scanning is gone, but a local rule pack can still declare
81
+ // category "secret"; findings from such a rule keep their matched text out of
82
+ // every report, the same courtesy the built-in rules got.
71
83
  function isSensitive(finding) {
72
84
  return finding?.category === 'secret'
73
85
  || finding?.matchedRules?.some((match) => match.category === 'secret') === true;
74
86
  }
75
87
 
76
88
  // Exit codes: 0 clean, 10 blocked, 11 review required, 31 incomplete scan.
77
- export function exitCode(findings, profile, complete = true) {
89
+ // An incomplete scan stops the operation only where no later guard can vouch
90
+ // for the skipped content: on the strict profile, or when the caller is the
91
+ // final ref boundary (failClosedIncomplete). Frictionless profiles warn and
92
+ // continue — the reference-transaction guard still scans introduced commits
93
+ // with failClosedIncomplete set, so the skipped content is checked before any
94
+ // ref moves.
95
+ export function exitCode(findings, profile, complete = true, { failClosedIncomplete = false } = {}) {
78
96
  let block = false;
79
97
  let review = false;
80
98
  for (const f of findings) {
@@ -82,7 +100,7 @@ export function exitCode(findings, profile, complete = true) {
82
100
  else if (f.decision === 'review') review = true;
83
101
  }
84
102
  if (block) return 10;
85
- if (!complete) return 31;
103
+ if (!complete && (profile === 'strict' || failClosedIncomplete)) return 31;
86
104
  if (review && findings.some((finding) => (finding.scanProfile || profile) !== 'clean')) return 11;
87
105
  return 0;
88
106
  }
@@ -52,10 +52,10 @@ export function scanEntries(repo, engine, entries, options = {}) {
52
52
  }
53
53
 
54
54
  // Oversized files: probe the first 8 KB to separate binary from text.
55
- // A binary file (PSD, WOFF, PNG) can't hide text-pattern secrets that
56
- // the full content scan would find, so it skips as 'binary' (complete).
57
- // A text file that exceeds the budget is a genuine 'size-limit' skip
58
- // (incomplete) and the caller must raise the limit to cover it.
55
+ // A binary file (PSD, WOFF, PNG) holds no text lines the content rules
56
+ // could match, so it skips as 'binary' (complete). A text file that
57
+ // exceeds the budget is a genuine 'size-limit' skip (incomplete) and the
58
+ // caller must raise the limit to cover it.
59
59
  // Cap probing at 16 MiB: cat-file --batch reads the full blob into memory,
60
60
  // so probing a 500 MB file just to check for NULs is wasteful. Files above
61
61
  // the cap are classified as size-limit without probing.
@@ -100,25 +100,18 @@ export function scanEntries(repo, engine, entries, options = {}) {
100
100
  // Binary detection still reads the blob. Count every examined byte so
101
101
  // later commits cannot reset the total budget merely by using NUL data.
102
102
  stats.bytes_scanned += blob.length;
103
- let matched;
104
103
  if (isBinary(blob)) {
104
+ // Content rules match text lines, so a binary blob carries nothing
105
+ // they could fire on. Skipping it loses no coverage: the skip is
106
+ // recorded as 'binary', which does not mark the scan incomplete.
105
107
  increment(stats.skipped, 'binary');
106
108
  appendPath(stats.skippedPaths, 'binary', entry.path, blob.length);
107
- // Binary classification only skips text-oriented policy rules. Secret
108
- // signatures are ASCII byte sequences, so latin1 preserves a
109
- // one-byte-to-one-code-unit view and keeps the existing byte limits.
110
- // Stripping NULs is what defeats hiding credential material behind
111
- // them: one injected NUL breaks a signature, and a multi-byte
112
- // encoding like UTF-16 injects one per character.
113
- matched = engine.checkContent(entry.path, blob.toString('latin1').replace(/\0/g, ''), {
114
- categories: ['secret'],
115
- });
116
- } else {
117
- stats.files_scanned += 1;
118
- const ranges = lineRanges.get(entry.path);
119
- matched = engine.checkContent(entry.path, blob.toString('utf8'),
120
- ranges && ranges.length ? { lineRanges: ranges } : {});
109
+ continue;
121
110
  }
111
+ stats.files_scanned += 1;
112
+ const ranges = lineRanges.get(entry.path);
113
+ const matched = engine.checkContent(entry.path, blob.toString('utf8'),
114
+ ranges && ranges.length ? { lineRanges: ranges } : {});
122
115
  stats.findings_total += matched.length;
123
116
  for (const finding of matched) {
124
117
  if (findings.length >= limits.maxFindings) continue;
@@ -253,8 +246,8 @@ function isBinary(buffer) {
253
246
  //
254
247
  // Entries without commit/parents (tracked snapshots, staged views, root
255
248
  // commits) are omitted from the map; the caller treats a missing key as "scan
256
- // the whole blob" — the safe side for a secret scanner is to scan MORE, not
257
- // less, so a failure to compute hunks falls back to the pre-W4 behaviour.
249
+ // the whole blob" — the safe side is to scan MORE, not less, so a failure to
250
+ // compute hunks falls back to the pre-W4 behaviour.
258
251
  function collectLineRanges(repo, entries) {
259
252
  // Group text-candidate entries by their first parent to batch the diffs.
260
253
  // Key by `${commit}\0${parent}` so a merge with multiple parents produces
@@ -389,8 +389,8 @@ function scanEntryGroup(repo, engine, entries, policy, accumulator, options = {}
389
389
  // Content scanning can target a narrower set than the path check. When
390
390
  // contentEntries is provided (e.g. only changed files in a commit), read
391
391
  // blobs only for those entries instead of the full snapshot. Path-based
392
- // rules already ran on the full tree above, so secrets like .env are still
393
- // caught even when their blob isn't re-read.
392
+ // rules already ran on the full tree above, so a path-only finding still
393
+ // fires even when its blob isn't re-read.
394
394
  const contentScannable = (options.contentEntries ?? entries)
395
395
  .filter((entry) => entry.status !== 'D' && entry.type !== 'deleted');
396
396
  const remaining = accumulator.remaining();
@@ -405,14 +405,14 @@ function scanEntryGroup(repo, engine, entries, policy, accumulator, options = {}
405
405
  function scanReviewPathChanges(engine, entries, policy, accumulator, options = {}) {
406
406
  for (const entry of entries) {
407
407
  // A deleted path is gone, so only structural policy rules (agent
408
- // instructions, project policy) can still matter: deleting a secret or a
409
- // session file is hygiene, not a violation. A renamed-away path is
410
- // different — its content survives under a new name, so a path-only
411
- // secret (e.g. .env) must still fire or a `git mv` to a neutral name
412
- // would slip past the destination scan, which only catches content-shaped
413
- // secrets. That finding is reported on the destination path (where the
414
- // bytes now live) so clean-profile repair unstages the blob that carries
415
- // the secret rather than the old name.
408
+ // instructions, project policy) can still matter: deleting a flagged
409
+ // file is hygiene, not a violation. A renamed-away path is different —
410
+ // its content survives under a new name, so a path-only secret-category
411
+ // rule from a local pack must still fire or a `git mv` to a neutral
412
+ // name would slip past the destination scan, which only catches
413
+ // content-shaped matches. That finding is reported on the destination
414
+ // path (where the bytes now live) so clean-profile repair unstages the
415
+ // blob that carries the match rather than the old name.
416
416
  const deleted = entry.status === 'D' || entry.type === 'deleted';
417
417
  const renamed = entry.status === 'R' && entry.sourcePath && entry.sourcePath !== entry.path;
418
418
  if (!deleted && !renamed) continue;
package/src/scan.mjs CHANGED
@@ -19,7 +19,6 @@ export class Engine {
19
19
  this.rules = rules;
20
20
  this.allowPaths = new Set();
21
21
  this.allowRules = new Set();
22
- this.allowSecretPaths = new Set();
23
22
  this.denyPaths = new Set();
24
23
  this.denyRules = new Set();
25
24
  this.scopedAllow = [];
@@ -33,7 +32,6 @@ export class Engine {
33
32
  const denied = this.#splitOverrides(deny);
34
33
  this.allowPaths = allowed.paths;
35
34
  this.allowRules = allowed.rules;
36
- this.allowSecretPaths = allowed.secretPaths;
37
35
  this.denyPaths = denied.paths;
38
36
  this.denyRules = denied.rules;
39
37
  this.scopedAllow = scopedAllow.filter((entry) => (
@@ -59,8 +57,8 @@ export class Engine {
59
57
  // - clean/compliance: review is ADVISORY (a stderr message, exit 0),
60
58
  // so re-surfacing it on every edit of a reviewed agent-instruction
61
59
  // file (CLAUDE.md, AGENTS.md) is pure friction with no security
62
- // payoff — the security boundary is secret scanning, strict blocks,
63
- // and reference-transaction. A LIVE-FILE review (newObjectId set)
60
+ // payoff — the security boundary is strict blocks and the
61
+ // reference-transaction guard. A LIVE-FILE review (newObjectId set)
64
62
  // therefore persists per path + rule across edits. A TOMBSTONE
65
63
  // review (newObjectId null — a reviewed deletion) keeps the exact
66
64
  // match in every profile: a deletion review must not silently cover
@@ -80,20 +78,13 @@ export class Engine {
80
78
  && entry.newObjectId === context.objectId
81
79
  && entry.newMode === context.mode);
82
80
  })) return 'allow';
83
- // A rule-level allow cannot bypass the secret-category guard that the
84
- // path-level allow below enforces: secret rules require an explicit
85
- // --scope secret-path override on a specific path, never a blanket rule allow.
86
- if (this.allowRules.has(rule.id) && rule.category !== 'secret') return 'allow';
87
- // A --scope secret-path allow is the explicit override for secret-category
88
- // rules only; it must not also suppress a non-secret rule that happens to
89
- // match the same path (principle of least privilege).
90
- if (this.allowSecretPaths.has(target) && rule.category === 'secret') return 'allow';
91
- if (this.allowPaths.has(target) && rule.category !== 'secret') return 'allow';
81
+ if (this.allowRules.has(rule.id)) return 'allow';
82
+ if (this.allowPaths.has(target)) return 'allow';
92
83
  return base;
93
84
  }
94
85
 
95
86
  #splitOverrides(entries) {
96
- const result = { paths: new Set(), rules: new Set(), secretPaths: new Set() };
87
+ const result = { paths: new Set(), rules: new Set() };
97
88
  for (const entry of entries || []) {
98
89
  const target = typeof entry === 'string' ? entry : entry?.target;
99
90
  if (!target) continue;
@@ -101,7 +92,6 @@ export class Engine {
101
92
  ? (this.lookup(target) ? 'rule' : 'path')
102
93
  : (entry.scope ?? (this.lookup(target) ? 'rule' : 'path'));
103
94
  if (scope === 'rule') result.rules.add(target);
104
- else if (scope === 'secret-path') result.secretPaths.add(target);
105
95
  else if (scope === 'path') result.paths.add(target);
106
96
  }
107
97
  return result;
@@ -138,13 +128,10 @@ export class Engine {
138
128
  checkContent(path, content, options = {}) {
139
129
  const out = [];
140
130
  const p = normalize(path);
141
- const categories = Array.isArray(options.categories)
142
- ? new Set(options.categories)
143
- : null;
144
131
  // lineRanges narrows content scanning to changed hunks (W4, bug 12d-F1).
145
132
  // When provided (an array of inclusive 1-based {start,end} ranges), only
146
133
  // lines falling in a range are evaluated; this stops a file that contains
147
- // a secret-bearing line ELSEWHERE (a PEM header inside a test string on
134
+ // a flagged line ELSEWHERE (a marker phrase inside a test fixture on
148
135
  // line 200) from blocking a commit that only edited line 50. Findings
149
136
  // keep their real line numbers because lineRecords already anchors them.
150
137
  // Absent = scan every line (the pre-W4 behaviour).
@@ -153,8 +140,8 @@ export class Engine {
153
140
  : null;
154
141
  for (const record of lineRecords(String(content))) {
155
142
  if (ranges && !ranges.some((range) => record.line >= range.start && record.line <= range.end)) continue;
156
- this.#markLocalInputSkip('code', p, record.text, categories);
157
- const result = this.#evaluate('code', p, record.text, { categories });
143
+ this.#markLocalInputSkip('code', p, record.text);
144
+ const result = this.#evaluate('code', p, record.text);
158
145
  if (result) out.push(finding(result, { path: p, line: record.line, text: record.text }));
159
146
  }
160
147
  return out;
@@ -198,11 +185,10 @@ export class Engine {
198
185
  return skipped;
199
186
  }
200
187
 
201
- #markLocalInputSkip(kind, target, text, categories = null) {
188
+ #markLocalInputSkip(kind, target, text) {
202
189
  if (text.length <= MAX_LOCAL_MATCH_INPUT) return;
203
190
  const applicable = this.rules.some((rule) => {
204
191
  if (rule.source !== 'local' || rule.kind !== kind) return false;
205
- if (categories && !categories.has(rule.category)) return false;
206
192
  if (kind !== 'code') return true;
207
193
  const candidate = rule.pathCaseInsensitive ? target.toLowerCase() : target;
208
194
  if (rule.pathRes.length && !rule.pathRes.some((regexp) => regexp.test(candidate))) return false;
@@ -216,7 +202,6 @@ export class Engine {
216
202
  for (const rule of this.rules) {
217
203
  if (rule.kind !== kind) continue;
218
204
  if (context.excludedRuleIds?.has(rule.id)) continue;
219
- if (context.categories && !context.categories.has(rule.category)) continue;
220
205
  const matched = kind === 'path'
221
206
  ? matchesPath(rule, target)
222
207
  : matchesContent(rule, text, kind === 'code' ? target : undefined);
package/src/state.mjs CHANGED
@@ -11,7 +11,7 @@ import { normalizeGitPath } from './git-path.mjs';
11
11
 
12
12
  const PROFILES = new Set(['clean', 'strict', 'compliance']);
13
13
  const OVERRIDE_SCOPES = new Set([
14
- 'path', 'rule', 'secret-path', 'reviewed-instruction', 'reviewed-policy-file', 'policy-migration',
14
+ 'path', 'rule', 'reviewed-instruction', 'reviewed-policy-file', 'policy-migration',
15
15
  ]);
16
16
  const OVERRIDE_FIELDS = new Set([
17
17
  'target', 'scope', 'reason', 'actor', 'at', 'head', 'transition', 'oldObjectId', 'newObjectId', 'newMode',
@@ -121,20 +121,29 @@ export function loadConfig(stateDir, root) {
121
121
  throw new LocalConfigError(file, `invalid JSON: ${error.message}`, error);
122
122
  }
123
123
  const normalized = normalizeLocalConfig(config, file);
124
- return { profile: normalized.profile, source: 'local', file };
124
+ return {
125
+ profile: normalized.profile,
126
+ source: 'local',
127
+ file,
128
+ ...(normalized.gitignore ? { gitignore: normalized.gitignore } : {}),
129
+ };
125
130
  }
126
131
 
127
132
  export function saveConfig(stateDir, config) {
128
133
  const file = join(stateDir, 'config.json');
129
134
  const normalized = normalizeLocalConfig(config, file);
130
- atomicWriteJson(file, { schema_version: 1, profile: normalized.profile });
135
+ atomicWriteJson(file, {
136
+ schema_version: 1,
137
+ profile: normalized.profile,
138
+ ...(normalized.gitignore ? { gitignore: normalized.gitignore } : {}),
139
+ });
131
140
  }
132
141
 
133
142
  function normalizeLocalConfig(config, file) {
134
143
  if (!config || typeof config !== 'object' || Array.isArray(config)) {
135
144
  throw new LocalConfigError(file, 'root must be a JSON object');
136
145
  }
137
- const unknown = Object.keys(config).filter((key) => !['schema_version', 'profile'].includes(key));
146
+ const unknown = Object.keys(config).filter((key) => !['schema_version', 'profile', 'gitignore'].includes(key));
138
147
  if (unknown.length) {
139
148
  throw new LocalConfigError(file, `unsupported field${unknown.length === 1 ? '' : 's'}: ${unknown.join(', ')}`);
140
149
  }
@@ -147,7 +156,32 @@ function normalizeLocalConfig(config, file) {
147
156
  if (!PROFILES.has(config.profile)) {
148
157
  throw new LocalConfigError(file, 'profile must be clean, strict, or compliance');
149
158
  }
150
- return { profile: config.profile };
159
+ const gitignore = normalizeGitignoreConfig(config.gitignore, file);
160
+ return { profile: config.profile, ...(gitignore ? { gitignore } : {}) };
161
+ }
162
+
163
+ // The gitignore field records an `init --gitignore` opt-in for this clone:
164
+ // enabled says the worktree .gitignore carries our managed block, created says
165
+ // the file did not exist before we wrote it (so uninstall may delete a file we
166
+ // introduced once the block leaves it empty). It stays out of the project
167
+ // policy on purpose — whether this clone created its .gitignore is per-clone
168
+ // state, never team policy. Absent means disabled, and a disabled record
169
+ // normalizes away so the two spellings cannot drift apart.
170
+ function normalizeGitignoreConfig(value, file) {
171
+ if (value === undefined) return undefined;
172
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
173
+ throw new LocalConfigError(file, 'gitignore must be an object');
174
+ }
175
+ const unknown = Object.keys(value).filter((key) => !['enabled', 'created'].includes(key));
176
+ if (unknown.length) {
177
+ throw new LocalConfigError(file, `gitignore has unsupported field${unknown.length === 1 ? '' : 's'}: ${unknown.join(', ')}`);
178
+ }
179
+ for (const field of ['enabled', 'created']) {
180
+ if (typeof value[field] !== 'boolean') {
181
+ throw new LocalConfigError(file, `gitignore.${field} must be a boolean`);
182
+ }
183
+ }
184
+ return value.enabled ? { enabled: true, created: value.created } : undefined;
151
185
  }
152
186
 
153
187
  export function loadOverrides(stateDir) {
@@ -196,18 +230,33 @@ function normalizeOverrides(value, file) {
196
230
  if (value.schema_version !== undefined && value.schema_version !== 1) {
197
231
  throw new LocalOverridesError(file, 'schema_version must be 1');
198
232
  }
199
- return {
200
- allow: overrideEntries(value.allow, 'allow', file),
201
- deny: overrideEntries(value.deny, 'deny', file),
233
+ const dropped = { count: 0 };
234
+ const normalized = {
235
+ allow: overrideEntries(value.allow, 'allow', file, dropped),
236
+ deny: overrideEntries(value.deny, 'deny', file, dropped),
202
237
  };
238
+ if (dropped.count) {
239
+ process.stderr.write(
240
+ `aimhooman: warning: ${file}: dropped ${dropped.count} override(s) with retired scope "secret-path"; built-in secret scanning was removed in v0.3.0\n`
241
+ );
242
+ }
243
+ return normalized;
203
244
  }
204
245
 
205
- function overrideEntries(value, key, file) {
246
+ function overrideEntries(value, key, file, dropped = { count: 0 }) {
206
247
  if (value === undefined) return [];
207
248
  if (!Array.isArray(value)) {
208
249
  throw new LocalOverridesError(file, `${key} must be an array`);
209
250
  }
210
251
  return value.map((entry, index) => {
252
+ // Built-in secret scanning and its secret-path override scope were
253
+ // removed in v0.3.0. An overrides file written by an older version may
254
+ // still carry such entries; drop them (one warning per file, below)
255
+ // instead of failing the whole load.
256
+ if (entry && typeof entry === 'object' && !Array.isArray(entry) && entry.scope === 'secret-path') {
257
+ dropped.count += 1;
258
+ return null;
259
+ }
211
260
  if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
212
261
  throw new LocalOverridesError(file, `${key}[${index}] must be an object`);
213
262
  }
@@ -260,7 +309,7 @@ function overrideEntries(value, key, file) {
260
309
  normalized.transition = normalized.transition.toLowerCase();
261
310
  }
262
311
  return normalized;
263
- });
312
+ }).filter((entry) => entry !== null);
264
313
  }
265
314
 
266
315
  function isRfc3339DateTime(value) {
@@ -1,92 +0,0 @@
1
- [
2
- {
3
- "id": "secret.private-key-content",
4
- "version": 1,
5
- "provider": "generic",
6
- "category": "secret",
7
- "confidence": "high",
8
- "kind": "code",
9
- "match": {
10
- "content": [
11
- "-----BEGIN (?:ENCRYPTED |RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY(?: BLOCK)?-----"
12
- ]
13
- },
14
- "actions": {
15
- "clean": "block",
16
- "strict": "block",
17
- "compliance": "block"
18
- },
19
- "reason": "Private-key material must not enter Git history.",
20
- "remediation": [
21
- "Remove the key from the index, rotate it if exposed, and store it outside the repository."
22
- ]
23
- },
24
- {
25
- "id": "secret.service-account-key",
26
- "version": 1,
27
- "provider": "generic",
28
- "category": "secret",
29
- "confidence": "high",
30
- "kind": "code",
31
- "match": {
32
- "content": [
33
- "(?i)\\\"private_key\\\"\\s*:\\s*\\\"-----BEGIN (?:RSA )?PRIVATE KEY-----"
34
- ]
35
- },
36
- "actions": {
37
- "clean": "block",
38
- "strict": "block",
39
- "compliance": "block"
40
- },
41
- "reason": "A service-account credential must not enter Git history.",
42
- "remediation": [
43
- "Remove the credential from the index, rotate it if exposed, and use the service's secret store."
44
- ]
45
- },
46
- {
47
- "id": "secret.aws-key-content",
48
- "version": 3,
49
- "provider": "aws",
50
- "category": "secret",
51
- "confidence": "high",
52
- "kind": "code",
53
- "match": {
54
- "content": [
55
- "(?i)\\baws_(?:secret_access_key|session_token)\\b\\s*[:=]\\s*[\\\"']?[A-Za-z0-9/+=]{32,}(?<!EXAMPLEKEY)(?![A-Za-z0-9/+=])",
56
- "\\b(?:AKIA|ASIA)(?![A-Z0-9]{9}EXAMPLE\\b)[A-Z0-9]{16}\\b"
57
- ]
58
- },
59
- "actions": {
60
- "clean": "block",
61
- "strict": "block",
62
- "compliance": "block"
63
- },
64
- "reason": "AWS credential material must not enter Git history.",
65
- "remediation": [
66
- "Remove the credential from the index, rotate it if exposed, and use an AWS credential provider."
67
- ]
68
- },
69
- {
70
- "id": "secret.provider-token",
71
- "version": 1,
72
- "provider": "generic",
73
- "category": "secret",
74
- "confidence": "high",
75
- "kind": "code",
76
- "match": {
77
- "content": [
78
- "\\b(?:gh[pousr]_[A-Za-z0-9]{36,255}|github_pat_[A-Za-z0-9_]{60,255}|glpat-[A-Za-z0-9_-]{20,255}|npm_[A-Za-z0-9]{36,255}|xox[baprs]-[A-Za-z0-9-]{20,255})\\b",
79
- "\\b(?:sk-ant-api[0-9]{2}-[A-Za-z0-9_-]{80,255}|sk-proj-[A-Za-z0-9_-]{40,255}|AIza[A-Za-z0-9_-]{35}|(?:sk|rk)_live_[A-Za-z0-9]{20,255}|hf_[A-Za-z0-9]{34}|SG\\.[A-Za-z0-9_-]{20,255}\\.[A-Za-z0-9_-]{40,255})\\b"
80
- ]
81
- },
82
- "actions": {
83
- "clean": "block",
84
- "strict": "block",
85
- "compliance": "block"
86
- },
87
- "reason": "A provider access token must not enter Git history.",
88
- "remediation": [
89
- "Remove the token from the index, revoke or rotate it, and use the provider's secret store."
90
- ]
91
- }
92
- ]