@promptci/core 0.1.0 → 0.2.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/dist/action-pinning.d.ts +37 -0
- package/dist/action-pinning.d.ts.map +1 -0
- package/dist/action-pinning.js +277 -0
- package/dist/action-pinning.js.map +1 -0
- package/dist/agent-practices.d.ts +1 -1
- package/dist/agent-practices.d.ts.map +1 -1
- package/dist/agent-practices.js +107 -43
- package/dist/agent-practices.js.map +1 -1
- package/dist/ai-config.d.ts +42 -3
- package/dist/ai-config.d.ts.map +1 -1
- package/dist/ai-config.js +210 -12
- package/dist/ai-config.js.map +1 -1
- package/dist/alias-files.d.ts +33 -0
- package/dist/alias-files.d.ts.map +1 -0
- package/dist/alias-files.js +60 -0
- package/dist/alias-files.js.map +1 -0
- package/dist/buried-critical.d.ts.map +1 -1
- package/dist/buried-critical.js +4 -9
- package/dist/buried-critical.js.map +1 -1
- package/dist/canonical-owner.d.ts.map +1 -1
- package/dist/canonical-owner.js +45 -25
- package/dist/canonical-owner.js.map +1 -1
- package/dist/ci-alignment.d.ts.map +1 -1
- package/dist/ci-alignment.js +3 -2
- package/dist/ci-alignment.js.map +1 -1
- package/dist/command-validity.d.ts +16 -0
- package/dist/command-validity.d.ts.map +1 -1
- package/dist/command-validity.js +337 -63
- package/dist/command-validity.js.map +1 -1
- package/dist/conflicts.d.ts.map +1 -1
- package/dist/conflicts.js +3 -2
- package/dist/conflicts.js.map +1 -1
- package/dist/context-bloat.d.ts +1 -1
- package/dist/context-bloat.d.ts.map +1 -1
- package/dist/context-bloat.js +18 -7
- package/dist/context-bloat.js.map +1 -1
- package/dist/context-optimizer.d.ts.map +1 -1
- package/dist/context-optimizer.js +5 -2
- package/dist/context-optimizer.js.map +1 -1
- package/dist/custom-rules.d.ts.map +1 -1
- package/dist/custom-rules.js +6 -1
- package/dist/custom-rules.js.map +1 -1
- package/dist/dead-references.d.ts +16 -2
- package/dist/dead-references.d.ts.map +1 -1
- package/dist/dead-references.js +635 -77
- package/dist/dead-references.js.map +1 -1
- package/dist/dependency-staleness.d.ts +35 -0
- package/dist/dependency-staleness.d.ts.map +1 -0
- package/dist/dependency-staleness.js +1001 -0
- package/dist/dependency-staleness.js.map +1 -0
- package/dist/detectors.d.ts.map +1 -1
- package/dist/detectors.js +31 -6
- package/dist/detectors.js.map +1 -1
- package/dist/duplicates.d.ts.map +1 -1
- package/dist/duplicates.js +5 -4
- package/dist/duplicates.js.map +1 -1
- package/dist/evidence.d.ts +11 -0
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +40 -0
- package/dist/evidence.js.map +1 -1
- package/dist/finding-id.d.ts +21 -0
- package/dist/finding-id.d.ts.map +1 -0
- package/dist/finding-id.js +31 -0
- package/dist/finding-id.js.map +1 -0
- package/dist/frameworks/dotnet.js +2 -2
- package/dist/frameworks/dotnet.js.map +1 -1
- package/dist/frameworks/rust.d.ts.map +1 -1
- package/dist/frameworks/rust.js +2 -1
- package/dist/frameworks/rust.js.map +1 -1
- package/dist/git-symlinks.d.ts +30 -0
- package/dist/git-symlinks.d.ts.map +1 -0
- package/dist/git-symlinks.js +161 -0
- package/dist/git-symlinks.js.map +1 -0
- package/dist/gitignore.d.ts +22 -0
- package/dist/gitignore.d.ts.map +1 -0
- package/dist/gitignore.js +157 -0
- package/dist/gitignore.js.map +1 -0
- package/dist/health-score.d.ts.map +1 -1
- package/dist/health-score.js +34 -5
- package/dist/health-score.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/makefile.d.ts +37 -0
- package/dist/makefile.d.ts.map +1 -0
- package/dist/makefile.js +156 -0
- package/dist/makefile.js.map +1 -0
- package/dist/manifest-consistency.d.ts.map +1 -1
- package/dist/manifest-consistency.js +52 -8
- package/dist/manifest-consistency.js.map +1 -1
- package/dist/negative-instructions.d.ts.map +1 -1
- package/dist/negative-instructions.js +4 -9
- package/dist/negative-instructions.js.map +1 -1
- package/dist/path-containment.d.ts +29 -0
- package/dist/path-containment.d.ts.map +1 -1
- package/dist/path-containment.js +70 -0
- package/dist/path-containment.js.map +1 -1
- package/dist/prompt-cache.d.ts.map +1 -1
- package/dist/prompt-cache.js +8 -6
- package/dist/prompt-cache.js.map +1 -1
- package/dist/repo-context.d.ts +53 -0
- package/dist/repo-context.d.ts.map +1 -1
- package/dist/repo-context.js +134 -11
- package/dist/repo-context.js.map +1 -1
- package/dist/report.d.ts +7 -0
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +36 -7
- package/dist/report.js.map +1 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +41 -2
- package/dist/scan.js.map +1 -1
- package/dist/scanner.d.ts +49 -1
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +323 -38
- package/dist/scanner.js.map +1 -1
- package/dist/security-pack.d.ts.map +1 -1
- package/dist/security-pack.js +6 -4
- package/dist/security-pack.js.map +1 -1
- package/dist/skill-supply-chain.d.ts +132 -0
- package/dist/skill-supply-chain.d.ts.map +1 -0
- package/dist/skill-supply-chain.js +2954 -0
- package/dist/skill-supply-chain.js.map +1 -0
- package/dist/skills-detector.d.ts +7 -3
- package/dist/skills-detector.d.ts.map +1 -1
- package/dist/skills-detector.js +98 -22
- package/dist/skills-detector.js.map +1 -1
- package/dist/stale-instructions.d.ts.map +1 -1
- package/dist/stale-instructions.js +2 -1
- package/dist/stale-instructions.js.map +1 -1
- package/dist/suppression.d.ts +2 -0
- package/dist/suppression.d.ts.map +1 -1
- package/dist/suppression.js +5 -2
- package/dist/suppression.js.map +1 -1
- package/dist/types.d.ts +35 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +22 -0
- package/dist/types.js.map +1 -1
- package/dist/vague-guidance.d.ts.map +1 -1
- package/dist/vague-guidance.js +2 -1
- package/dist/vague-guidance.js.map +1 -1
- package/dist/windsurf-detector.d.ts.map +1 -1
- package/dist/windsurf-detector.js +15 -6
- package/dist/windsurf-detector.js.map +1 -1
- package/dist/within-section-dedup.d.ts.map +1 -1
- package/dist/within-section-dedup.js +4 -9
- package/dist/within-section-dedup.js.map +1 -1
- package/dist/workspace-manifests.d.ts +80 -0
- package/dist/workspace-manifests.d.ts.map +1 -0
- package/dist/workspace-manifests.js +333 -0
- package/dist/workspace-manifests.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { RepoContext } from './repo-context.js';
|
|
2
|
+
import type { PromptCiIssue } from './types.js';
|
|
3
|
+
export type ActionUse = {
|
|
4
|
+
/** The `uses:` value as written (quotes and trailing comment stripped). */
|
|
5
|
+
ref: string;
|
|
6
|
+
/** 1-based line number. */
|
|
7
|
+
line: number;
|
|
8
|
+
};
|
|
9
|
+
export type UnpinnedUse = ActionUse & {
|
|
10
|
+
/**
|
|
11
|
+
* False when the ref's text does not match the plain `owner/repo[/path]@ref`
|
|
12
|
+
* shape. Such a ref is still reported, but its raw text is never
|
|
13
|
+
* interpolated into titles, evidence, or the recommended `gh` command — it
|
|
14
|
+
* came from the scanned repo and may carry shell or Markdown metacharacters.
|
|
15
|
+
*/
|
|
16
|
+
displayable: boolean;
|
|
17
|
+
/** `owner/repo[/path]` part before the `@` (only meaningful when displayable). */
|
|
18
|
+
action: string;
|
|
19
|
+
/** Git ref after the `@` (only meaningful when displayable). */
|
|
20
|
+
version: string;
|
|
21
|
+
owner: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Every step/job-level `uses:` reference in a workflow or composite-action
|
|
25
|
+
* file, in line order. Skipped: comments, lines inside block scalars (a
|
|
26
|
+
* `run: |` script that prints `uses: foo@v1`), and `uses` keys nested under
|
|
27
|
+
* data mappings such as `with:`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function extractActionUses(content: string): ActionUse[];
|
|
30
|
+
/**
|
|
31
|
+
* Classify one `uses:` value. Returns the parts for a remote ref that should
|
|
32
|
+
* be pinned but is not, or `undefined` when it is pinned, local, a container,
|
|
33
|
+
* or an expression.
|
|
34
|
+
*/
|
|
35
|
+
export declare function unpinnedUse(use: ActionUse): UnpinnedUse | undefined;
|
|
36
|
+
export declare function detectActionPinning(context: RepoContext): PromptCiIssue[];
|
|
37
|
+
//# sourceMappingURL=action-pinning.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action-pinning.d.ts","sourceRoot":"","sources":["../src/action-pinning.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAkB,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAwDhD,MAAM,MAAM,SAAS,GAAG;IACtB,2EAA2E;IAC3E,GAAG,EAAE,MAAM,CAAC;IACZ,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG;IACpC;;;;;OAKG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AA6DF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CA6C9D;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,CAoBnE;AA6ED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,WAAW,GAAG,aAAa,EAAE,CAkBzE"}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* GitHub Actions pinning detector (detect-only).
|
|
5
|
+
*
|
|
6
|
+
* Flags `uses:` references in `.github/workflows/*.yml` and in composite
|
|
7
|
+
* actions (`action.yml` at the repo root or under `.github/actions/`) that
|
|
8
|
+
* point at a tag or branch instead of a full commit SHA. A tag is a mutable
|
|
9
|
+
* pointer: whoever can push to the action's repository can move it, and every
|
|
10
|
+
* workflow referencing it runs the new code on its next run. A SHA cannot be
|
|
11
|
+
* repointed.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately DETECT-ONLY: there is no fix recipe and `autoApplySafe` is never
|
|
14
|
+
* set. Resolving a tag to its SHA needs a network call, and this package makes
|
|
15
|
+
* none from detector code — pinning is left to the user (or Dependabot /
|
|
16
|
+
* Renovate, which keep a pinned SHA and its version comment in step).
|
|
17
|
+
*
|
|
18
|
+
* Exempt: local actions (`./path`) and container actions (`docker://…`), which
|
|
19
|
+
* have no git ref to pin. These files are read from `workflows.sources`, never
|
|
20
|
+
* from `context.files`, so they are never treated as instruction files.
|
|
21
|
+
*
|
|
22
|
+
* Deterministic and offline: line-oriented parsing of the YAML text, no clock,
|
|
23
|
+
* no network.
|
|
24
|
+
*/
|
|
25
|
+
/** A full git commit id: 40-hex SHA-1, or 64-hex for SHA-256 repositories. */
|
|
26
|
+
const FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
|
|
27
|
+
/** `owner/repo` or `owner/repo/sub/path` — the only action shape shown verbatim. */
|
|
28
|
+
const ACTION_RE = /^[\w.-]+\/[\w.-]+(?:\/[\w./-]+)?$/;
|
|
29
|
+
/** A plausible tag/branch name — the only version shape shown verbatim. */
|
|
30
|
+
const VERSION_RE = /^[\w./+-]+$/;
|
|
31
|
+
/**
|
|
32
|
+
* Owners whose actions GitHub itself maintains. An unpinned first-party ref is
|
|
33
|
+
* still a mutable pointer, but the account holding it is GitHub's own, so it
|
|
34
|
+
* is reported one severity lower than a third-party ref.
|
|
35
|
+
*/
|
|
36
|
+
const FIRST_PARTY_OWNERS = new Set(['actions', 'github']);
|
|
37
|
+
/** A `key: value` mapping line, optionally the first key of a list item. */
|
|
38
|
+
const KEY_LINE_RE = /^(\s*)(-\s+)?([\w.-]+)\s*:(?:\s+(.*))?$/;
|
|
39
|
+
/** A list item whose value is a flow mapping: `- { uses: x@v1, with: {...} }`. */
|
|
40
|
+
const FLOW_ITEM_RE = /^\s*-\s+(\{.*)$/;
|
|
41
|
+
/** A block scalar indicator (`|`, `>-`, `|2+`), optionally followed by a comment. */
|
|
42
|
+
const BLOCK_SCALAR_RE = /^[|>][+-]?\d*\s*(?:#.*)?$/;
|
|
43
|
+
/**
|
|
44
|
+
* Keys whose block-mapping values are free-form data, not steps. A `uses:` key
|
|
45
|
+
* nested under them (e.g. an input literally named `uses`) is not a step.
|
|
46
|
+
*/
|
|
47
|
+
const DATA_MAPPING_KEYS = new Set(['with', 'env', 'secrets', 'inputs', 'outputs']);
|
|
48
|
+
/** Strip a trailing ` # comment` and surrounding quotes from a scalar value. */
|
|
49
|
+
function scalarValue(raw) {
|
|
50
|
+
let value = raw.trim();
|
|
51
|
+
const quote = value[0];
|
|
52
|
+
if (quote === '"' || quote === "'") {
|
|
53
|
+
const end = value.indexOf(quote, 1);
|
|
54
|
+
return end > 0 ? value.slice(1, end) : value.slice(1);
|
|
55
|
+
}
|
|
56
|
+
const comment = value.search(/(?:^|\s)#/);
|
|
57
|
+
if (comment >= 0)
|
|
58
|
+
value = value.slice(0, comment);
|
|
59
|
+
return value.trim();
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The value of a top-level `uses` key in a single-line flow mapping, or
|
|
63
|
+
* undefined. Nested maps (`with: { uses: … }`) are depth > 1 and ignored.
|
|
64
|
+
*/
|
|
65
|
+
function flowMappingUses(flow) {
|
|
66
|
+
const entries = [];
|
|
67
|
+
let depth = 0;
|
|
68
|
+
let quote = '';
|
|
69
|
+
let current = '';
|
|
70
|
+
for (const c of flow) {
|
|
71
|
+
if (quote) {
|
|
72
|
+
if (c === quote)
|
|
73
|
+
quote = '';
|
|
74
|
+
if (depth === 1)
|
|
75
|
+
current += c;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (c === '"' || c === "'") {
|
|
79
|
+
quote = c;
|
|
80
|
+
if (depth === 1)
|
|
81
|
+
current += c;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (c === '{' || c === '[') {
|
|
85
|
+
depth++;
|
|
86
|
+
if (depth > 1)
|
|
87
|
+
current += c;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (c === '}' || c === ']') {
|
|
91
|
+
depth--;
|
|
92
|
+
if (depth === 0)
|
|
93
|
+
break;
|
|
94
|
+
current += c;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (c === ',' && depth === 1) {
|
|
98
|
+
entries.push(current);
|
|
99
|
+
current = '';
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (depth >= 1)
|
|
103
|
+
current += c;
|
|
104
|
+
}
|
|
105
|
+
entries.push(current);
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
const m = /^\s*uses\s*:\s*(.*)$/.exec(entry);
|
|
108
|
+
if (m)
|
|
109
|
+
return scalarValue(m[1]);
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Every step/job-level `uses:` reference in a workflow or composite-action
|
|
115
|
+
* file, in line order. Skipped: comments, lines inside block scalars (a
|
|
116
|
+
* `run: |` script that prints `uses: foo@v1`), and `uses` keys nested under
|
|
117
|
+
* data mappings such as `with:`.
|
|
118
|
+
*/
|
|
119
|
+
export function extractActionUses(content) {
|
|
120
|
+
const lines = content.split(/\r?\n/);
|
|
121
|
+
const uses = [];
|
|
122
|
+
// Lines indented deeper than this column belong to a block scalar or data
|
|
123
|
+
// mapping and are skipped; -1 = not inside one.
|
|
124
|
+
let skipDeeperThan = -1;
|
|
125
|
+
for (let i = 0; i < lines.length; i++) {
|
|
126
|
+
const line = lines[i];
|
|
127
|
+
if (line.trim() === '')
|
|
128
|
+
continue;
|
|
129
|
+
const indent = /^\s*/.exec(line)[0].length;
|
|
130
|
+
if (skipDeeperThan >= 0) {
|
|
131
|
+
if (indent > skipDeeperThan)
|
|
132
|
+
continue;
|
|
133
|
+
skipDeeperThan = -1;
|
|
134
|
+
}
|
|
135
|
+
if (/^\s*#/.test(line))
|
|
136
|
+
continue;
|
|
137
|
+
const flow = FLOW_ITEM_RE.exec(line);
|
|
138
|
+
if (flow) {
|
|
139
|
+
const ref = flowMappingUses(flow[1]);
|
|
140
|
+
if (ref)
|
|
141
|
+
uses.push({ ref, line: i + 1 });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const kv = KEY_LINE_RE.exec(line);
|
|
145
|
+
if (!kv)
|
|
146
|
+
continue;
|
|
147
|
+
const keyCol = kv[1].length + (kv[2]?.length ?? 0);
|
|
148
|
+
const key = kv[3];
|
|
149
|
+
const rawValue = kv[4] ?? '';
|
|
150
|
+
const value = rawValue.trim();
|
|
151
|
+
if (key === 'uses') {
|
|
152
|
+
const ref = scalarValue(rawValue);
|
|
153
|
+
if (ref)
|
|
154
|
+
uses.push({ ref, line: i + 1 });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (BLOCK_SCALAR_RE.test(value)) {
|
|
158
|
+
skipDeeperThan = keyCol;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if ((value === '' || value.startsWith('#')) && DATA_MAPPING_KEYS.has(key)) {
|
|
162
|
+
skipDeeperThan = keyCol;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return uses;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Classify one `uses:` value. Returns the parts for a remote ref that should
|
|
169
|
+
* be pinned but is not, or `undefined` when it is pinned, local, a container,
|
|
170
|
+
* or an expression.
|
|
171
|
+
*/
|
|
172
|
+
export function unpinnedUse(use) {
|
|
173
|
+
const { ref } = use;
|
|
174
|
+
if (ref.startsWith('./') || ref.startsWith('.\\'))
|
|
175
|
+
return undefined; // local action
|
|
176
|
+
if (ref.startsWith('docker://'))
|
|
177
|
+
return undefined; // container action
|
|
178
|
+
if (ref.includes('${{'))
|
|
179
|
+
return undefined; // expression — not statically resolvable
|
|
180
|
+
const at = ref.lastIndexOf('@');
|
|
181
|
+
if (at <= 0 || !ref.slice(0, at).includes('/'))
|
|
182
|
+
return undefined; // not a remote action ref
|
|
183
|
+
const action = ref.slice(0, at);
|
|
184
|
+
const version = ref.slice(at + 1);
|
|
185
|
+
if (FULL_SHA_RE.test(version))
|
|
186
|
+
return undefined;
|
|
187
|
+
const displayable = ACTION_RE.test(action) && VERSION_RE.test(version);
|
|
188
|
+
return {
|
|
189
|
+
...use,
|
|
190
|
+
displayable,
|
|
191
|
+
action,
|
|
192
|
+
version,
|
|
193
|
+
owner: displayable ? action.split('/')[0].toLowerCase() : '',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function shortHash(value) {
|
|
197
|
+
return crypto.createHash('sha1').update(value).digest('hex').slice(0, 12);
|
|
198
|
+
}
|
|
199
|
+
/** Belt and braces for text that is shown inside Markdown code spans. */
|
|
200
|
+
function codeSafe(text) {
|
|
201
|
+
return text.replace(/`/g, "'");
|
|
202
|
+
}
|
|
203
|
+
function buildIssue(context, site) {
|
|
204
|
+
const { filePath, lines, use } = site;
|
|
205
|
+
const absPath = path.resolve(context.repoRoot, filePath);
|
|
206
|
+
const base = {
|
|
207
|
+
// One finding per (file, ref): pinning the ref in one workflow must not
|
|
208
|
+
// re-key the finding for the other files that still use it.
|
|
209
|
+
id: `action-pin-${shortHash(`action-pin:${filePath}:${use.ref}`)}`,
|
|
210
|
+
category: 'security',
|
|
211
|
+
// Absolute, like every other finding's paths; the JSON report relativizes.
|
|
212
|
+
filePaths: [absPath],
|
|
213
|
+
locations: lines.map((line) => ({ filePath: absPath, startLine: line, endLine: line })),
|
|
214
|
+
// The ref is certainly mutable; whether that matters for this repo is the
|
|
215
|
+
// heuristic part, so confidence stays below 1 and the score hit modest
|
|
216
|
+
// (and capped — see TAG_DEDUCTION_CAP in health-score.ts).
|
|
217
|
+
confidence: 0.8,
|
|
218
|
+
tags: ['supply-chain', 'github-actions'],
|
|
219
|
+
};
|
|
220
|
+
const why = "A tag or branch is a mutable pointer: whoever can push to the action's repository can move " +
|
|
221
|
+
"it, and the next run executes the new code with this repository's token and secrets.";
|
|
222
|
+
if (!use.displayable) {
|
|
223
|
+
// The raw value never reaches the title, evidence, or a shell command.
|
|
224
|
+
return {
|
|
225
|
+
...base,
|
|
226
|
+
severity: 'warning',
|
|
227
|
+
title: 'GitHub Action reference is not pinned to a commit SHA',
|
|
228
|
+
summary: `${filePath} has a \`uses:\` reference (line ${lines.join(', ')}) that is not pinned to a full ` +
|
|
229
|
+
`commit SHA and does not look like a plain \`owner/repo@ref\` value. ${why}`,
|
|
230
|
+
// No line numbers (baseline fingerprints hash evidence); a hash of the
|
|
231
|
+
// raw value keeps two different refs in one file distinct.
|
|
232
|
+
evidence: [`${filePath}: unrecognised uses: value (sha1 ${shortHash(use.ref)})`],
|
|
233
|
+
recommendation: 'Review this `uses:` line by hand. Pin the action to the full 40-character commit SHA of a ' +
|
|
234
|
+
"release you trust (resolved from the action's own repository), keep the version in a trailing " +
|
|
235
|
+
'comment, and let Dependabot or Renovate update both. PromptCI reports this but does not rewrite ' +
|
|
236
|
+
'workflow files.',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const firstParty = FIRST_PARTY_OWNERS.has(use.owner);
|
|
240
|
+
const [repoOwner, repoName] = use.action.split('/');
|
|
241
|
+
const ref = codeSafe(use.ref);
|
|
242
|
+
return {
|
|
243
|
+
...base,
|
|
244
|
+
severity: firstParty ? 'info' : 'warning',
|
|
245
|
+
title: `GitHub Action \`${ref}\` is not pinned to a commit SHA`,
|
|
246
|
+
summary: `${filePath} references \`${codeSafe(use.action)}\` at \`${codeSafe(use.version)}\`, which appears ` +
|
|
247
|
+
`to be a tag or branch rather than a full commit SHA. ${why} ` +
|
|
248
|
+
(firstParty
|
|
249
|
+
? 'This action is maintained by GitHub, which lowers but does not remove the risk.'
|
|
250
|
+
: 'Consider pinning third-party actions to a reviewed commit.'),
|
|
251
|
+
evidence: [`${filePath}: uses: ${ref}`],
|
|
252
|
+
recommendation: 'Pin the action to the full commit SHA of the release you trust and keep the version in a ' +
|
|
253
|
+
`trailing comment, e.g. \`uses: ${codeSafe(use.action)}@<40-char-sha> # ${codeSafe(use.version)}\`. ` +
|
|
254
|
+
`Resolve the SHA from the action's own repository (\`gh api repos/${repoOwner}/${repoName}/commits/` +
|
|
255
|
+
`${use.version} --jq .sha\`), and let Dependabot or Renovate update the SHA and comment together. ` +
|
|
256
|
+
'PromptCI reports this but does not rewrite workflow files.',
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
export function detectActionPinning(context) {
|
|
260
|
+
const sources = context.workflows.sources ?? [];
|
|
261
|
+
// Group occurrences by (file, ref): several jobs in one workflow using the
|
|
262
|
+
// same unpinned action are one fix, and one finding.
|
|
263
|
+
const sites = new Map();
|
|
264
|
+
for (const source of sources) {
|
|
265
|
+
for (const use of extractActionUses(source.content)) {
|
|
266
|
+
const unpinned = unpinnedUse(use);
|
|
267
|
+
if (!unpinned)
|
|
268
|
+
continue;
|
|
269
|
+
const key = `${source.filePath}\u0000${unpinned.ref}`;
|
|
270
|
+
const site = sites.get(key) ?? { filePath: source.filePath, lines: [], use: unpinned };
|
|
271
|
+
site.lines.push(unpinned.line);
|
|
272
|
+
sites.set(key, site);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return [...sites.keys()].sort().map((key) => buildIssue(context, sites.get(key)));
|
|
276
|
+
}
|
|
277
|
+
//# sourceMappingURL=action-pinning.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action-pinning.js","sourceRoot":"","sources":["../src/action-pinning.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,5 +20,5 @@
|
|
|
20
20
|
* All findings are info/warning severity — these are guidance gaps, not errors.
|
|
21
21
|
*/
|
|
22
22
|
import type { InstructionFile, PromptCiIssue } from './types.js';
|
|
23
|
-
export declare function detectAgentPractices(files: InstructionFile[]): PromptCiIssue[];
|
|
23
|
+
export declare function detectAgentPractices(files: InstructionFile[], aliases?: InstructionFile[]): PromptCiIssue[];
|
|
24
24
|
//# sourceMappingURL=agent-practices.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-practices.d.ts","sourceRoot":"","sources":["../src/agent-practices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-practices.d.ts","sourceRoot":"","sources":["../src/agent-practices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAscjE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,eAAe,EAAE,EAAE,OAAO,GAAE,eAAe,EAAO,GAAG,aAAa,EAAE,CAwS/G"}
|
package/dist/agent-practices.js
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
import * as crypto from 'node:crypto';
|
|
23
23
|
import * as path from 'node:path';
|
|
24
24
|
import { stripCodeBlocks } from './markdown-fences.js';
|
|
25
|
+
import { fileIdPath } from './finding-id.js';
|
|
26
|
+
import { aliasesWithNewType, groupAliasesByTarget } from './alias-files.js';
|
|
25
27
|
const CHECKS = [
|
|
26
28
|
{
|
|
27
29
|
id: 'no-verification-loop',
|
|
@@ -225,6 +227,12 @@ const CLAUDE_XML_TAG_RE = /<\/?claude:[\w-]+|<instructions-for-claude[\s>]/i;
|
|
|
225
227
|
// and must not exempt the file. The `.md` extension is required so prose like
|
|
226
228
|
// "use Claude to review" cannot match the bare word.
|
|
227
229
|
const FORWARDING_RE = /(?:follow|use|see|refer\s+to|canonical\s+(?:source|instructions?)|pointer\s+to)\s+[`'"*]*(?:\.?\/)?(?:\.github\/)?(?:AGENTS|CLAUDE)\.md\b/i;
|
|
230
|
+
// The same delegation phrased subject-first: "`AGENTS.md` is the single source
|
|
231
|
+
// of truth for all agents" / "AGENTS.md is canonical". Common in the tool stubs
|
|
232
|
+
// (GEMINI.md, .clinerules) that rule-sync setups generate next to AGENTS.md.
|
|
233
|
+
// Same-line only, so an unrelated "canonical" further down cannot pair up; and
|
|
234
|
+
// a file naming ITSELF as the source of truth is not delegating (see caller).
|
|
235
|
+
const SOURCE_OF_TRUTH_RE = /\b(AGENTS|CLAUDE)\.md\b[^\n]{0,60}?\b(?:single\s+source\s+of\s+truth|canonical)\b/i;
|
|
228
236
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
229
237
|
function issueId(checkId) {
|
|
230
238
|
const hash = crypto.createHash('sha1').update(checkId).digest('hex').slice(0, 12);
|
|
@@ -313,6 +321,18 @@ function perFileBehaviorTarget(file) {
|
|
|
313
321
|
if (isCopilotInstructionsFile(file)) {
|
|
314
322
|
return { label: '.github/copilot-instructions.md', agent: 'GitHub Copilot' };
|
|
315
323
|
}
|
|
324
|
+
// GEMINI.md and a single-file `.clinerules` are read standalone by their
|
|
325
|
+
// tool, exactly like the files above — the same BUG-19 reasoning as
|
|
326
|
+
// `.windsurfrules`. Files inside a `.clinerules/` DIRECTORY are not targets:
|
|
327
|
+
// Cline concatenates the whole directory, so one rule file lacking guidance
|
|
328
|
+
// that a sibling provides is not a gap. Persona files (SOUL.md etc.) are not
|
|
329
|
+
// tool-specific rule files, so they are not per-agent targets either.
|
|
330
|
+
if (file.fileType === 'gemini') {
|
|
331
|
+
return { label: 'GEMINI.md', agent: 'Gemini CLI' };
|
|
332
|
+
}
|
|
333
|
+
if (file.fileType === 'cline' && path.basename(file.path) === '.clinerules') {
|
|
334
|
+
return { label: '.clinerules', agent: 'Cline' };
|
|
335
|
+
}
|
|
316
336
|
return undefined;
|
|
317
337
|
}
|
|
318
338
|
function isForwardingFile(file) {
|
|
@@ -324,13 +344,76 @@ function isForwardingFile(file) {
|
|
|
324
344
|
const head = file.content.split(/\r?\n/).slice(0, 12).join('\n');
|
|
325
345
|
if (FORWARDING_RE.test(head))
|
|
326
346
|
return true;
|
|
347
|
+
const sourceOfTruth = SOURCE_OF_TRUTH_RE.exec(head);
|
|
348
|
+
if (sourceOfTruth && `${sourceOfTruth[1].toLowerCase()}.md` !== path.basename(file.path).toLowerCase()) {
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
327
351
|
// Legacy: a short file with a forwarding pointer anywhere.
|
|
328
352
|
return file.lineCount < 20 && FORWARDING_RE.test(file.content);
|
|
329
353
|
}
|
|
330
354
|
// ── Detector ─────────────────────────────────────────────────────────────────
|
|
331
|
-
|
|
355
|
+
/**
|
|
356
|
+
* Symlink aliases whose OWN tool-specific name adds a per-agent target the real
|
|
357
|
+
* file does not have: `CLAUDE.md -> AGENTS.md` means Claude reads AGENTS.md's
|
|
358
|
+
* content, so the per-file behavior checks apply to it as the CLAUDE.md it is.
|
|
359
|
+
* An alias whose target is the same kind of file as the real one adds nothing.
|
|
360
|
+
*/
|
|
361
|
+
function aliasesWithNewTarget(files, aliases) {
|
|
362
|
+
return aliases.filter((alias) => {
|
|
363
|
+
const target = perFileBehaviorTarget(alias);
|
|
364
|
+
if (!target)
|
|
365
|
+
return false;
|
|
366
|
+
const canonical = files.find((f) => f.relativePath === alias.aliasOf);
|
|
367
|
+
return canonical === undefined || perFileBehaviorTarget(canonical)?.label !== target.label;
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
/** A Claude XML tag is only meaningful in a CLAUDE.md; README files document the tags as examples. */
|
|
371
|
+
function isClaudeTagTarget(fileType) {
|
|
372
|
+
return fileType !== 'claude' && fileType !== 'readme';
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* BUG-007: Claude-specific XML tags in a non-Claude instruction file.
|
|
376
|
+
* <claude:thinking> and similar tags are Claude-specific features; embedding them
|
|
377
|
+
* in .cursorrules or AGENTS.md is tool-specific syntax in the wrong file type.
|
|
378
|
+
* Skip README files — they often document Claude XML tags as examples rather than using them
|
|
379
|
+
* Skip if the tag only appears inside fenced code blocks (documentation, not real usage)
|
|
380
|
+
*/
|
|
381
|
+
function claudeXmlTagIssue(names) {
|
|
382
|
+
// `names` is one real file, or every link to one real file (one finding listing them all, id from the first).
|
|
383
|
+
const file = names[0];
|
|
384
|
+
if (!isClaudeTagTarget(file.fileType))
|
|
385
|
+
return undefined;
|
|
386
|
+
const contentOutsideCode = stripCodeBlocks(file.content).replace(/`[^`\n]+`/g, '');
|
|
387
|
+
if (!CLAUDE_XML_TAG_RE.test(contentOutsideCode))
|
|
388
|
+
return undefined;
|
|
389
|
+
const hash = crypto.createHash('sha1').update(`claude-xml-tag:${fileIdPath(file)}`).digest('hex').slice(0, 12);
|
|
390
|
+
// AP3: search contentOutsideCode (the same text the gate above tested),
|
|
391
|
+
// not the raw file.content — otherwise an earlier occurrence of the
|
|
392
|
+
// tag that exists ONLY inside a code fence (documentation, not real
|
|
393
|
+
// usage) could be picked as evidence instead of the real match that
|
|
394
|
+
// actually made the gate fire.
|
|
395
|
+
const firstTag = (/<\/?claude:[\w-]+/i).exec(contentOutsideCode)?.[0] ?? '<claude:...>';
|
|
396
|
+
return {
|
|
397
|
+
id: `claude-xml-tag-${hash}`,
|
|
398
|
+
severity: 'warning',
|
|
399
|
+
category: 'structure',
|
|
400
|
+
title: 'Claude-specific XML tag in non-Claude instruction file',
|
|
401
|
+
summary: `This file contains a Claude-specific XML tag (${firstTag}) but is not a CLAUDE.md file. ` +
|
|
402
|
+
`Claude XML tags are only interpreted by Claude; other AI tools (Cursor, Copilot, etc.) ` +
|
|
403
|
+
`will treat them as literal text or ignore them.`,
|
|
404
|
+
filePaths: names.map((f) => f.path),
|
|
405
|
+
locations: names.map((f) => ({ filePath: f.path })),
|
|
406
|
+
evidence: [`Found: ${firstTag} in ${[...new Set(names.map((f) => f.fileType))].join(', ')} file`],
|
|
407
|
+
recommendation: 'Move Claude-specific directives to CLAUDE.md, or replace this tag with a plain-text ' +
|
|
408
|
+
'instruction that all tools can understand.',
|
|
409
|
+
confidence: 0.85,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
export function detectAgentPractices(files, aliases = []) {
|
|
332
413
|
if (files.length === 0)
|
|
333
414
|
return [];
|
|
415
|
+
// Files the per-agent checks run over: every scanned file plus the symlink aliases that are a different tool's file.
|
|
416
|
+
const targetFiles = [...files, ...aliasesWithNewTarget(files, aliases)];
|
|
334
417
|
const issues = [];
|
|
335
418
|
const combinedContent = files.map((f) => f.content).join('\n');
|
|
336
419
|
// BUG-002: For the verification-loop check, strip human-facing checklist items
|
|
@@ -378,7 +461,7 @@ export function detectAgentPractices(files) {
|
|
|
378
461
|
// granular "missing critical guidance" issues (one per CHECKS entry),
|
|
379
462
|
// inflating the health-score penalty ~6x for a single underlying gap.
|
|
380
463
|
const filesWithNoGuidanceFinding = new Set();
|
|
381
|
-
for (const file of
|
|
464
|
+
for (const file of targetFiles) {
|
|
382
465
|
const target = perFileBehaviorTarget(file);
|
|
383
466
|
if (!target)
|
|
384
467
|
continue;
|
|
@@ -396,7 +479,7 @@ export function detectAgentPractices(files) {
|
|
|
396
479
|
continue;
|
|
397
480
|
filesWithNoGuidanceFinding.add(file.path);
|
|
398
481
|
issues.push({
|
|
399
|
-
id: perFileBehaviorIssueId(file
|
|
482
|
+
id: perFileBehaviorIssueId(fileIdPath(file)),
|
|
400
483
|
severity: 'warning',
|
|
401
484
|
category: 'agent_practices',
|
|
402
485
|
title: `No behavioral guidance in ${target.label}`,
|
|
@@ -424,7 +507,7 @@ export function detectAgentPractices(files) {
|
|
|
424
507
|
const sourceFile = files.find((f) => matchesAny(stripChecklistLines(f.content), check.patterns));
|
|
425
508
|
if (!sourceFile)
|
|
426
509
|
continue;
|
|
427
|
-
for (const file of
|
|
510
|
+
for (const file of targetFiles) {
|
|
428
511
|
if (file === sourceFile)
|
|
429
512
|
continue;
|
|
430
513
|
const target = perFileBehaviorTarget(file);
|
|
@@ -439,7 +522,7 @@ export function detectAgentPractices(files) {
|
|
|
439
522
|
const hasThisGuidance = matchesAny(stripChecklistLines(file.content), check.patterns);
|
|
440
523
|
if (!hasThisGuidance) {
|
|
441
524
|
const checkName = check.title.toLowerCase().replace('no ', '').replace(' instruction', '');
|
|
442
|
-
const hash = crypto.createHash('sha1').update(`behavior-gap:${file
|
|
525
|
+
const hash = crypto.createHash('sha1').update(`behavior-gap:${fileIdPath(file)}:${check.id}`).digest('hex').slice(0, 12);
|
|
443
526
|
issues.push({
|
|
444
527
|
id: `per-file-behavioral-gap-${hash}`,
|
|
445
528
|
severity: 'warning',
|
|
@@ -488,7 +571,7 @@ export function detectAgentPractices(files) {
|
|
|
488
571
|
if (hasGeneralBehaviorOnlyInCopilot()) {
|
|
489
572
|
const copilotFile = files.find(isCopilotInstructionsFile);
|
|
490
573
|
issues.push({
|
|
491
|
-
id: copilotAgentBehaviorId(copilotFile
|
|
574
|
+
id: copilotAgentBehaviorId(fileIdPath(copilotFile)),
|
|
492
575
|
severity: 'warning',
|
|
493
576
|
category: 'structure',
|
|
494
577
|
title: 'General behavioral guidance placed only in Copilot instructions',
|
|
@@ -511,10 +594,10 @@ export function detectAgentPractices(files) {
|
|
|
511
594
|
if (!matchesAny(file.content, KEY_BEHAVIORAL_PATTERNS))
|
|
512
595
|
continue;
|
|
513
596
|
// Check if we already flagged this via general-behavior-only check
|
|
514
|
-
if (issues.some(i => i.id === copilotAgentBehaviorId(file
|
|
597
|
+
if (issues.some(i => i.id === copilotAgentBehaviorId(fileIdPath(file))))
|
|
515
598
|
continue;
|
|
516
599
|
issues.push({
|
|
517
|
-
id: copilotAgentBehaviorId(file
|
|
600
|
+
id: copilotAgentBehaviorId(fileIdPath(file)),
|
|
518
601
|
severity: 'warning',
|
|
519
602
|
category: 'structure',
|
|
520
603
|
title: 'Agent-behavior section in copilot-instructions.md',
|
|
@@ -545,7 +628,7 @@ export function detectAgentPractices(files) {
|
|
|
545
628
|
if (hasDoNotEdit && !hasGeneratorNote) {
|
|
546
629
|
// Case 1 (original): "do not edit" present but NO generator note.
|
|
547
630
|
issues.push({
|
|
548
|
-
id: doNotEditIssueId(file
|
|
631
|
+
id: doNotEditIssueId(fileIdPath(file)),
|
|
549
632
|
severity: 'warning',
|
|
550
633
|
category: 'agent_practices',
|
|
551
634
|
title: '"Do not edit" header in human-maintained instruction file',
|
|
@@ -568,7 +651,7 @@ export function detectAgentPractices(files) {
|
|
|
568
651
|
return `auto-gen-header-${hash}`;
|
|
569
652
|
};
|
|
570
653
|
issues.push({
|
|
571
|
-
id: autoGenId(file
|
|
654
|
+
id: autoGenId(fileIdPath(file)),
|
|
572
655
|
severity: 'warning',
|
|
573
656
|
category: 'agent_practices',
|
|
574
657
|
title: 'Misleading auto-generated header on human-maintained instruction file',
|
|
@@ -583,39 +666,20 @@ export function detectAgentPractices(files) {
|
|
|
583
666
|
confidence: 0.75,
|
|
584
667
|
});
|
|
585
668
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
// usage) could be picked as evidence instead of the real match that
|
|
601
|
-
// actually made the gate fire.
|
|
602
|
-
const firstTag = (/<\/?claude:[\w-]+/i).exec(contentOutsideCode)?.[0] ?? '<claude:...>';
|
|
603
|
-
issues.push({
|
|
604
|
-
id: claudeTagId(file.path),
|
|
605
|
-
severity: 'warning',
|
|
606
|
-
category: 'structure',
|
|
607
|
-
title: 'Claude-specific XML tag in non-Claude instruction file',
|
|
608
|
-
summary: `This file contains a Claude-specific XML tag (${firstTag}) but is not a CLAUDE.md file. ` +
|
|
609
|
-
`Claude XML tags are only interpreted by Claude; other AI tools (Cursor, Copilot, etc.) ` +
|
|
610
|
-
`will treat them as literal text or ignore them.`,
|
|
611
|
-
filePaths: [file.path],
|
|
612
|
-
locations: [{ filePath: file.path }],
|
|
613
|
-
evidence: [`Found: ${firstTag} in ${file.fileType} file`],
|
|
614
|
-
recommendation: 'Move Claude-specific directives to CLAUDE.md, or replace this tag with a plain-text ' +
|
|
615
|
-
'instruction that all tools can understand.',
|
|
616
|
-
confidence: 0.85,
|
|
617
|
-
});
|
|
618
|
-
}
|
|
669
|
+
const claudeTag = claudeXmlTagIssue([file]);
|
|
670
|
+
if (claudeTag)
|
|
671
|
+
issues.push(claudeTag);
|
|
672
|
+
}
|
|
673
|
+
// The Claude-tag check is gated by file type, and `AGENTS.md -> CLAUDE.md` is an AGENTS.md to every
|
|
674
|
+
// tool that reads that name: run it for such an alias, against the real content. (An alias of a file
|
|
675
|
+
// that is already "not Claude" would only repeat the real file's finding, so it is left out.) The
|
|
676
|
+
// other per-type content checks above are not repeated for aliases: "do not edit" is type-independent,
|
|
677
|
+
// and the Copilot agent-behavior check assumes copilot-instructions.md is Copilot-only, which a link
|
|
678
|
+
// to AGENTS.md is not. Several links to one file give ONE finding that lists every link.
|
|
679
|
+
for (const group of groupAliasesByTarget(aliasesWithNewType(files, aliases, isClaudeTagTarget))) {
|
|
680
|
+
const claudeTag = claudeXmlTagIssue(group);
|
|
681
|
+
if (claudeTag)
|
|
682
|
+
issues.push(claudeTag);
|
|
619
683
|
}
|
|
620
684
|
return issues;
|
|
621
685
|
}
|