@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/.agents/rules/aimhooman.md +9 -6
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/.clinerules/aimhooman.md +9 -6
- package/.codex-plugin/plugin.json +3 -3
- package/.cursor/rules/aimhooman.mdc +9 -6
- package/.github/copilot-instructions.md +9 -6
- package/.kiro/steering/aimhooman.md +9 -6
- package/.windsurf/rules/aimhooman.md +9 -6
- package/AGENTS.md +9 -6
- package/CHANGELOG.md +147 -0
- package/CONTRIBUTING.md +2 -3
- package/GEMINI.md +9 -6
- package/README.md +56 -374
- package/SECURITY.md +6 -4
- package/bin/aimhooman.mjs +190 -55
- package/docs/ai-artifacts.gitignore +63 -0
- package/docs/catalog.md +41 -0
- package/docs/design/frictionless-enforcement.md +27 -17
- package/hooks/hooks.json +1 -1
- package/package.json +5 -2
- package/rules/paths.json +0 -114
- package/schemas/overrides.schema.json +4 -1
- package/skills/aimhooman/SKILL.md +11 -8
- package/src/exclude.mjs +16 -2
- package/src/githooks.mjs +38 -3
- package/src/gitx.mjs +68 -6
- package/src/hook.mjs +33 -12
- package/src/report.mjs +23 -5
- package/src/scan-session.mjs +14 -21
- package/src/scan-target.mjs +10 -10
- package/src/scan.mjs +9 -24
- package/src/state.mjs +59 -10
- package/rules/secrets.json +0 -92
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/scan-session.mjs
CHANGED
|
@@ -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)
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
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
|
-
|
|
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
|
|
257
|
-
//
|
|
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
|
package/src/scan-target.mjs
CHANGED
|
@@ -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
|
|
393
|
-
//
|
|
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
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
// would slip past the destination scan, which only catches
|
|
413
|
-
//
|
|
414
|
-
// bytes now live) so clean-profile repair unstages the
|
|
415
|
-
// the
|
|
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
|
|
63
|
-
//
|
|
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
|
-
|
|
84
|
-
|
|
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()
|
|
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
|
|
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
|
|
157
|
-
const result = this.#evaluate('code', p, record.text
|
|
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
|
|
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', '
|
|
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 {
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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) {
|
package/rules/secrets.json
DELETED
|
@@ -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
|
-
]
|