@rmyndharis/aimhooman 0.1.6 → 0.1.8
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +108 -0
- package/README.md +2 -2
- package/bin/aimhooman.mjs +368 -242
- package/docs/design/frictionless-enforcement.md +7 -4
- package/package.json +1 -1
- package/rules/paths.json +65 -16
- package/rules/secrets.json +3 -3
- package/src/githooks.mjs +51 -6
- package/src/gitx.mjs +10 -0
- package/src/hook.mjs +137 -5
- package/src/report.mjs +27 -2
- package/src/scan-session.mjs +77 -1
- package/src/scan-target.mjs +23 -5
- package/src/scan.mjs +40 -7
package/src/scan-target.mjs
CHANGED
|
@@ -215,21 +215,39 @@ function scanCommit(repo, options) {
|
|
|
215
215
|
if (floor && !isVersionedStrict(rawPolicy)) {
|
|
216
216
|
accumulator.add([protectedPolicyFinding(rawPolicy, policy, snapshot)]);
|
|
217
217
|
}
|
|
218
|
-
|
|
218
|
+
// A commit is judged by what it changes, not by the whole tree it inherits.
|
|
219
|
+
// Scanning snapshot.entries (a full `ls-tree`) re-applied path rules to
|
|
220
|
+
// every file the commit merely carried forward from its parent, so a path
|
|
221
|
+
// allowed into history once (under an override that was later removed, or
|
|
222
|
+
// before the guard existed) blocked every later commit on the branch. The
|
|
223
|
+
// change set already drives content scanning; routing path rules through it
|
|
224
|
+
// too means a newly added `.env` still fires while an inherited one stays
|
|
225
|
+
// silent, matching how scanRange has always judged history.
|
|
226
|
+
scanEntryGroup(repo, loaded.engine, snapshot.changes, policy, accumulator, {
|
|
219
227
|
reviewPathEntries: snapshot.changes,
|
|
220
228
|
contentEntries: snapshot.changes,
|
|
221
229
|
allowMissingPolicy: acknowledged && !rawPolicy.policy_object_id,
|
|
222
230
|
transition: snapshot.commit,
|
|
223
231
|
});
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
232
|
+
// The message is the author's words, so it is scanned for attribution and
|
|
233
|
+
// markers only when the commit was written here. Fetched history (a PR
|
|
234
|
+
// checked out for review, a branch pulled from a remote) carries other
|
|
235
|
+
// people's commit text that a local developer cannot edit, so flagging it
|
|
236
|
+
// only blocks the review. cmdRefcheck passes messageScope='changes-only'
|
|
237
|
+
// for commits that were imported rather than authored; a direct `aimhooman
|
|
238
|
+
// check --commit` still defaults to scanning the message.
|
|
239
|
+
const scanMessageText = options.messageScope !== 'changes-only';
|
|
240
|
+
if (scanMessageText) {
|
|
241
|
+
accumulator.add(loaded.engine.checkMessage(snapshot.message)
|
|
242
|
+
.map((finding) => decorate(finding, snapshot, policy)));
|
|
243
|
+
accumulator.addSkipped(loaded.engine.takeSkipped());
|
|
244
|
+
}
|
|
227
245
|
return result({
|
|
228
246
|
target: `commit:${snapshot.commit}`,
|
|
229
247
|
policy,
|
|
230
248
|
accumulator,
|
|
231
249
|
diagnostics,
|
|
232
|
-
messageScanned:
|
|
250
|
+
messageScanned: scanMessageText,
|
|
233
251
|
commit: snapshot.commit,
|
|
234
252
|
});
|
|
235
253
|
}
|
package/src/scan.mjs
CHANGED
|
@@ -51,13 +51,35 @@ export class Engine {
|
|
|
51
51
|
decide(base, target, rule, context = {}) {
|
|
52
52
|
if (this.denyPaths.has(target) || this.denyRules.has(rule.id)) return 'block';
|
|
53
53
|
if (context.transientAllowRules?.has(rule.id)) return 'allow';
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
// W11 (F2): review binding is profile-dependent.
|
|
55
|
+
// - strict: review binds the EXACT reviewed blob. A changed/copied/
|
|
56
|
+
// symlinked/mode-flipped target is NOT covered — it must be re-
|
|
57
|
+
// reviewed. This is the security property the strict lifecycle tests
|
|
58
|
+
// pin, and strict is the profile that exits 11 on review.
|
|
59
|
+
// - clean/compliance: review is ADVISORY (a stderr message, exit 0),
|
|
60
|
+
// so re-surfacing it on every edit of a reviewed agent-instruction
|
|
61
|
+
// 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)
|
|
64
|
+
// therefore persists per path + rule across edits. A TOMBSTONE
|
|
65
|
+
// review (newObjectId null — a reviewed deletion) keeps the exact
|
|
66
|
+
// match in every profile: a deletion review must not silently cover
|
|
67
|
+
// a later re-adding of the file.
|
|
68
|
+
const strictReview = this.profile === 'strict';
|
|
69
|
+
if (this.scopedAllow.some((entry) => {
|
|
70
|
+
if (entry.target !== target || entry.ruleId !== rule.id) return false;
|
|
71
|
+
if (entry.newObjectId === null) {
|
|
72
|
+
// Tombstone: exact match in every profile.
|
|
73
|
+
return entry.transition === context.transition
|
|
74
|
+
&& entry.newObjectId === context.objectId
|
|
75
|
+
&& entry.newMode === context.mode;
|
|
76
|
+
}
|
|
77
|
+
// Live-file review: exact in strict; path-anchored in clean/compliance.
|
|
78
|
+
return !strictReview
|
|
79
|
+
|| (entry.transition === context.transition
|
|
80
|
+
&& entry.newObjectId === context.objectId
|
|
81
|
+
&& entry.newMode === context.mode);
|
|
82
|
+
})) return 'allow';
|
|
61
83
|
// A rule-level allow cannot bypass the secret-category guard that the
|
|
62
84
|
// path-level allow below enforces: secret rules require an explicit
|
|
63
85
|
// --scope secret-path override on a specific path, never a blanket rule allow.
|
|
@@ -119,7 +141,18 @@ export class Engine {
|
|
|
119
141
|
const categories = Array.isArray(options.categories)
|
|
120
142
|
? new Set(options.categories)
|
|
121
143
|
: null;
|
|
144
|
+
// lineRanges narrows content scanning to changed hunks (W4, bug 12d-F1).
|
|
145
|
+
// When provided (an array of inclusive 1-based {start,end} ranges), only
|
|
146
|
+
// 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
|
|
148
|
+
// line 200) from blocking a commit that only edited line 50. Findings
|
|
149
|
+
// keep their real line numbers because lineRecords already anchors them.
|
|
150
|
+
// Absent = scan every line (the pre-W4 behaviour).
|
|
151
|
+
const ranges = Array.isArray(options.lineRanges) && options.lineRanges.length
|
|
152
|
+
? options.lineRanges
|
|
153
|
+
: null;
|
|
122
154
|
for (const record of lineRecords(String(content))) {
|
|
155
|
+
if (ranges && !ranges.some((range) => record.line >= range.start && record.line <= range.end)) continue;
|
|
123
156
|
this.#markLocalInputSkip('code', p, record.text, categories);
|
|
124
157
|
const result = this.#evaluate('code', p, record.text, { categories });
|
|
125
158
|
if (result) out.push(finding(result, { path: p, line: record.line, text: record.text }));
|