@rmyndharis/aimhooman 0.2.0 → 0.3.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.
- 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 +10 -7
- 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 +111 -0
- package/CONTRIBUTING.md +2 -3
- package/GEMINI.md +9 -6
- package/README.md +57 -376
- package/SECURITY.md +6 -4
- package/bin/aimhooman.mjs +134 -143
- package/docs/ai-artifacts.gitignore +63 -0
- package/docs/catalog.md +41 -0
- package/docs/cli-reference.md +114 -0
- package/docs/design/frictionless-enforcement.md +27 -17
- package/docs/faq.md +60 -0
- package/docs/integrations.md +108 -0
- package/docs/policy.md +82 -0
- package/docs/secrets.md +83 -0
- package/hooks/hooks.json +1 -1
- package/package.json +10 -2
- package/rules/paths.json +0 -114
- package/schemas/overrides.schema.json +4 -1
- package/skills/aimhooman/SKILL.md +11 -8
- package/src/args.mjs +2 -7
- package/src/exclude.mjs +2 -2
- package/src/gitx.mjs +1 -13
- package/src/hook.mjs +44 -12
- package/src/report.mjs +17 -41
- package/src/scan-session.mjs +15 -22
- package/src/scan-target.mjs +10 -10
- package/src/scan.mjs +9 -24
- package/src/state.mjs +104 -15
- package/rules/secrets.json +0 -96
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
|
@@ -4,14 +4,14 @@ import {
|
|
|
4
4
|
readFileSync,
|
|
5
5
|
} from 'node:fs';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
|
-
import { atomicWrite } from './atomic-write.mjs';
|
|
7
|
+
import { atomicWrite, withLock } from './atomic-write.mjs';
|
|
8
8
|
import { normalizeGitPath } from './git-path.mjs';
|
|
9
9
|
|
|
10
10
|
// Per-repository state, stored in the common Git dir (never the worktree).
|
|
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',
|
|
@@ -105,8 +105,20 @@ export function parseProjectPolicy(text, file = '.aimhooman.json') {
|
|
|
105
105
|
|
|
106
106
|
export function loadConfig(stateDir, root) {
|
|
107
107
|
const project = loadProjectPolicy(root);
|
|
108
|
-
if (project) return { profile: project.profile, source: 'project', file: project.file };
|
|
109
108
|
const file = join(stateDir, 'config.json');
|
|
109
|
+
if (project) {
|
|
110
|
+
// The project policy owns the profile, but the --gitignore opt-in stays
|
|
111
|
+
// per-clone state in config.json; a two-arg caller still needs it. The
|
|
112
|
+
// profile no longer depends on that file here, so a missing or corrupt
|
|
113
|
+
// one just omits the record instead of failing the load.
|
|
114
|
+
const gitignore = readGitignoreRecord(file);
|
|
115
|
+
return {
|
|
116
|
+
profile: project.profile,
|
|
117
|
+
source: 'project',
|
|
118
|
+
file: project.file,
|
|
119
|
+
...(gitignore ? { gitignore } : {}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
110
122
|
let text;
|
|
111
123
|
try {
|
|
112
124
|
text = readFileSync(file, 'utf8');
|
|
@@ -121,20 +133,40 @@ export function loadConfig(stateDir, root) {
|
|
|
121
133
|
throw new LocalConfigError(file, `invalid JSON: ${error.message}`, error);
|
|
122
134
|
}
|
|
123
135
|
const normalized = normalizeLocalConfig(config, file);
|
|
124
|
-
return {
|
|
136
|
+
return {
|
|
137
|
+
profile: normalized.profile,
|
|
138
|
+
source: 'local',
|
|
139
|
+
file,
|
|
140
|
+
...(normalized.gitignore ? { gitignore: normalized.gitignore } : {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Best-effort read of the per-clone gitignore record for the project-policy
|
|
145
|
+
// branch above: any failure (absent file, invalid JSON, invalid shape) means
|
|
146
|
+
// no record, never a failed load.
|
|
147
|
+
function readGitignoreRecord(file) {
|
|
148
|
+
try {
|
|
149
|
+
return normalizeLocalConfig(JSON.parse(stripBom(readFileSync(file, 'utf8'))), file).gitignore;
|
|
150
|
+
} catch {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
125
153
|
}
|
|
126
154
|
|
|
127
155
|
export function saveConfig(stateDir, config) {
|
|
128
156
|
const file = join(stateDir, 'config.json');
|
|
129
157
|
const normalized = normalizeLocalConfig(config, file);
|
|
130
|
-
atomicWriteJson(file, {
|
|
158
|
+
atomicWriteJson(file, {
|
|
159
|
+
schema_version: 1,
|
|
160
|
+
profile: normalized.profile,
|
|
161
|
+
...(normalized.gitignore ? { gitignore: normalized.gitignore } : {}),
|
|
162
|
+
});
|
|
131
163
|
}
|
|
132
164
|
|
|
133
165
|
function normalizeLocalConfig(config, file) {
|
|
134
166
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
135
167
|
throw new LocalConfigError(file, 'root must be a JSON object');
|
|
136
168
|
}
|
|
137
|
-
const unknown = Object.keys(config).filter((key) => !['schema_version', 'profile'].includes(key));
|
|
169
|
+
const unknown = Object.keys(config).filter((key) => !['schema_version', 'profile', 'gitignore'].includes(key));
|
|
138
170
|
if (unknown.length) {
|
|
139
171
|
throw new LocalConfigError(file, `unsupported field${unknown.length === 1 ? '' : 's'}: ${unknown.join(', ')}`);
|
|
140
172
|
}
|
|
@@ -147,7 +179,32 @@ function normalizeLocalConfig(config, file) {
|
|
|
147
179
|
if (!PROFILES.has(config.profile)) {
|
|
148
180
|
throw new LocalConfigError(file, 'profile must be clean, strict, or compliance');
|
|
149
181
|
}
|
|
150
|
-
|
|
182
|
+
const gitignore = normalizeGitignoreConfig(config.gitignore, file);
|
|
183
|
+
return { profile: config.profile, ...(gitignore ? { gitignore } : {}) };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The gitignore field records an `init --gitignore` opt-in for this clone:
|
|
187
|
+
// enabled says the worktree .gitignore carries our managed block, created says
|
|
188
|
+
// the file did not exist before we wrote it (so uninstall may delete a file we
|
|
189
|
+
// introduced once the block leaves it empty). It stays out of the project
|
|
190
|
+
// policy on purpose — whether this clone created its .gitignore is per-clone
|
|
191
|
+
// state, never team policy. Absent means disabled, and a disabled record
|
|
192
|
+
// normalizes away so the two spellings cannot drift apart.
|
|
193
|
+
function normalizeGitignoreConfig(value, file) {
|
|
194
|
+
if (value === undefined) return undefined;
|
|
195
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
196
|
+
throw new LocalConfigError(file, 'gitignore must be an object');
|
|
197
|
+
}
|
|
198
|
+
const unknown = Object.keys(value).filter((key) => !['enabled', 'created'].includes(key));
|
|
199
|
+
if (unknown.length) {
|
|
200
|
+
throw new LocalConfigError(file, `gitignore has unsupported field${unknown.length === 1 ? '' : 's'}: ${unknown.join(', ')}`);
|
|
201
|
+
}
|
|
202
|
+
for (const field of ['enabled', 'created']) {
|
|
203
|
+
if (typeof value[field] !== 'boolean') {
|
|
204
|
+
throw new LocalConfigError(file, `gitignore.${field} must be a boolean`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return value.enabled ? { enabled: true, created: value.created } : undefined;
|
|
151
208
|
}
|
|
152
209
|
|
|
153
210
|
export function loadOverrides(stateDir) {
|
|
@@ -159,7 +216,28 @@ export function loadOverrides(stateDir) {
|
|
|
159
216
|
if (error?.code === 'ENOENT') return { allow: [], deny: [] };
|
|
160
217
|
throw new LocalOverridesError(file, `cannot read file: ${error.message}`, error);
|
|
161
218
|
}
|
|
162
|
-
|
|
219
|
+
const dropped = { count: 0 };
|
|
220
|
+
const overrides = parseOverrides(text, file, dropped);
|
|
221
|
+
if (dropped.count) {
|
|
222
|
+
process.stderr.write(
|
|
223
|
+
`aimhooman: warning: ${file}: dropped ${dropped.count} override(s) with retired scope "secret-path"; built-in secret scanning was removed in v0.3.0\n`
|
|
224
|
+
);
|
|
225
|
+
// Persist the cleaned file so the migration warns once, not on every
|
|
226
|
+
// command. Re-read under the lock so a concurrent allow/deny write is
|
|
227
|
+
// never clobbered; a failed rewrite only means the warning returns
|
|
228
|
+
// next run, so this stays best effort. One attempt, no queue wait:
|
|
229
|
+
// allow/deny/override/review/policy-review call loadOverrides while
|
|
230
|
+
// already holding this same lock, and the bakery queue would park the
|
|
231
|
+
// inner candidate behind the outer one for the full retry budget
|
|
232
|
+
// before the catch below swallowed the throw.
|
|
233
|
+
try {
|
|
234
|
+
withLock(`${file}.lock`, () => {
|
|
235
|
+
const fresh = parseOverrides(readFileSync(file, 'utf8'), file);
|
|
236
|
+
atomicWriteJson(file, { schema_version: 1, ...fresh });
|
|
237
|
+
}, { retries: 1 });
|
|
238
|
+
} catch { /* best effort */ }
|
|
239
|
+
}
|
|
240
|
+
return overrides;
|
|
163
241
|
}
|
|
164
242
|
|
|
165
243
|
export function saveOverrides(stateDir, overrides) {
|
|
@@ -173,17 +251,17 @@ export function normalizeOverrideTarget(target) {
|
|
|
173
251
|
return normalizeGitPath(target);
|
|
174
252
|
}
|
|
175
253
|
|
|
176
|
-
function parseOverrides(text, file) {
|
|
254
|
+
function parseOverrides(text, file, dropped = { count: 0 }) {
|
|
177
255
|
let value;
|
|
178
256
|
try {
|
|
179
257
|
value = JSON.parse(stripBom(text));
|
|
180
258
|
} catch (error) {
|
|
181
259
|
throw new LocalOverridesError(file, `invalid JSON: ${error.message}`, error);
|
|
182
260
|
}
|
|
183
|
-
return normalizeOverrides(value, file);
|
|
261
|
+
return normalizeOverrides(value, file, dropped);
|
|
184
262
|
}
|
|
185
263
|
|
|
186
|
-
function normalizeOverrides(value, file) {
|
|
264
|
+
function normalizeOverrides(value, file, dropped = { count: 0 }) {
|
|
187
265
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
188
266
|
throw new LocalOverridesError(file, 'root must be a JSON object');
|
|
189
267
|
}
|
|
@@ -196,18 +274,29 @@ function normalizeOverrides(value, file) {
|
|
|
196
274
|
if (value.schema_version !== undefined && value.schema_version !== 1) {
|
|
197
275
|
throw new LocalOverridesError(file, 'schema_version must be 1');
|
|
198
276
|
}
|
|
277
|
+
// The caller owns the drop count: loadOverrides warns and persists the
|
|
278
|
+
// cleaned file, saveOverrides drops retired entries silently on the next
|
|
279
|
+
// write.
|
|
199
280
|
return {
|
|
200
|
-
allow: overrideEntries(value.allow, 'allow', file),
|
|
201
|
-
deny: overrideEntries(value.deny, 'deny', file),
|
|
281
|
+
allow: overrideEntries(value.allow, 'allow', file, dropped),
|
|
282
|
+
deny: overrideEntries(value.deny, 'deny', file, dropped),
|
|
202
283
|
};
|
|
203
284
|
}
|
|
204
285
|
|
|
205
|
-
function overrideEntries(value, key, file) {
|
|
286
|
+
function overrideEntries(value, key, file, dropped = { count: 0 }) {
|
|
206
287
|
if (value === undefined) return [];
|
|
207
288
|
if (!Array.isArray(value)) {
|
|
208
289
|
throw new LocalOverridesError(file, `${key} must be an array`);
|
|
209
290
|
}
|
|
210
291
|
return value.map((entry, index) => {
|
|
292
|
+
// Built-in secret scanning and its secret-path override scope were
|
|
293
|
+
// removed in v0.3.0. An overrides file written by an older version may
|
|
294
|
+
// still carry such entries; drop them (loadOverrides warns once and
|
|
295
|
+
// persists the cleaned file) instead of failing the whole load.
|
|
296
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry) && entry.scope === 'secret-path') {
|
|
297
|
+
dropped.count += 1;
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
211
300
|
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
212
301
|
throw new LocalOverridesError(file, `${key}[${index}] must be an object`);
|
|
213
302
|
}
|
|
@@ -260,7 +349,7 @@ function overrideEntries(value, key, file) {
|
|
|
260
349
|
normalized.transition = normalized.transition.toLowerCase();
|
|
261
350
|
}
|
|
262
351
|
return normalized;
|
|
263
|
-
});
|
|
352
|
+
}).filter((entry) => entry !== null);
|
|
264
353
|
}
|
|
265
354
|
|
|
266
355
|
function isRfc3339DateTime(value) {
|
package/rules/secrets.json
DELETED
|
@@ -1,96 +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
|
-
"If this is an intentional fixture, run: aimhooman allow <path> --scope secret-path --reason \"test fixture\"."
|
|
23
|
-
]
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"id": "secret.service-account-key",
|
|
27
|
-
"version": 1,
|
|
28
|
-
"provider": "generic",
|
|
29
|
-
"category": "secret",
|
|
30
|
-
"confidence": "high",
|
|
31
|
-
"kind": "code",
|
|
32
|
-
"match": {
|
|
33
|
-
"content": [
|
|
34
|
-
"(?i)\\\"private_key\\\"\\s*:\\s*\\\"-----BEGIN (?:RSA )?PRIVATE KEY-----"
|
|
35
|
-
]
|
|
36
|
-
},
|
|
37
|
-
"actions": {
|
|
38
|
-
"clean": "block",
|
|
39
|
-
"strict": "block",
|
|
40
|
-
"compliance": "block"
|
|
41
|
-
},
|
|
42
|
-
"reason": "A service-account credential must not enter Git history.",
|
|
43
|
-
"remediation": [
|
|
44
|
-
"Remove the credential from the index, rotate it if exposed, and use the service's secret store.",
|
|
45
|
-
"If this is an intentional fixture, run: aimhooman allow <path> --scope secret-path --reason \"test fixture\"."
|
|
46
|
-
]
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"id": "secret.aws-key-content",
|
|
50
|
-
"version": 3,
|
|
51
|
-
"provider": "aws",
|
|
52
|
-
"category": "secret",
|
|
53
|
-
"confidence": "high",
|
|
54
|
-
"kind": "code",
|
|
55
|
-
"match": {
|
|
56
|
-
"content": [
|
|
57
|
-
"(?i)\\baws_(?:secret_access_key|session_token)\\b\\s*[:=]\\s*[\\\"']?[A-Za-z0-9/+=]{32,}(?<!EXAMPLEKEY)(?![A-Za-z0-9/+=])",
|
|
58
|
-
"\\b(?:AKIA|ASIA)(?![A-Z0-9]{9}EXAMPLE\\b)[A-Z0-9]{16}\\b"
|
|
59
|
-
]
|
|
60
|
-
},
|
|
61
|
-
"actions": {
|
|
62
|
-
"clean": "block",
|
|
63
|
-
"strict": "block",
|
|
64
|
-
"compliance": "block"
|
|
65
|
-
},
|
|
66
|
-
"reason": "AWS credential material must not enter Git history.",
|
|
67
|
-
"remediation": [
|
|
68
|
-
"Remove the credential from the index, rotate it if exposed, and use an AWS credential provider.",
|
|
69
|
-
"If this is an intentional fixture, run: aimhooman allow <path> --scope secret-path --reason \"test fixture\"."
|
|
70
|
-
]
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
"id": "secret.provider-token",
|
|
74
|
-
"version": 1,
|
|
75
|
-
"provider": "generic",
|
|
76
|
-
"category": "secret",
|
|
77
|
-
"confidence": "high",
|
|
78
|
-
"kind": "code",
|
|
79
|
-
"match": {
|
|
80
|
-
"content": [
|
|
81
|
-
"\\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",
|
|
82
|
-
"\\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"
|
|
83
|
-
]
|
|
84
|
-
},
|
|
85
|
-
"actions": {
|
|
86
|
-
"clean": "block",
|
|
87
|
-
"strict": "block",
|
|
88
|
-
"compliance": "block"
|
|
89
|
-
},
|
|
90
|
-
"reason": "A provider access token must not enter Git history.",
|
|
91
|
-
"remediation": [
|
|
92
|
-
"Remove the token from the index, revoke or rotate it, and use the provider's secret store.",
|
|
93
|
-
"If this is an intentional fixture, run: aimhooman allow <path> --scope secret-path --reason \"test fixture\"."
|
|
94
|
-
]
|
|
95
|
-
}
|
|
96
|
-
]
|