coding-agent-skills 0.2.10 → 0.2.12
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/CHANGELOG.md +40 -0
- package/README.md +8 -0
- package/ROADMAP.md +9 -5
- package/bin/coding-agent-skills +14 -0
- package/docs/adapters/README.md +38 -0
- package/docs/adapters/project-installation.md +24 -0
- package/docs/adapters/real-project-adoption.md +2 -2
- package/docs/architecture/README.md +5 -3
- package/docs/release/README.md +3 -2
- package/docs/release/npm-package.md +10 -2
- package/docs/safety/README.md +11 -1
- package/docs/testing/README.md +15 -0
- package/docs/usage/README.md +23 -5
- package/examples/command-policies/api-contract-audit.json +70 -0
- package/examples/command-policies/secret-audit.json +71 -0
- package/examples/evidence-packs/api-contract-audit.json +60 -0
- package/examples/evidence-packs/secret-audit.json +55 -0
- package/examples/manifests/api-contract-audit.json +14 -0
- package/examples/manifests/secret-audit.json +14 -0
- package/examples/workflows/api-contract-audit.md +8 -0
- package/examples/workflows/secret-audit.md +10 -0
- package/package.json +3 -1
- package/runs/skill-runs.md +32 -0
- package/schemas/project-adapter-installation.schema.json +4 -0
- package/schemas/project-adapter.schema.json +4 -0
- package/scripts/lib/api-contract-audit.mjs +651 -0
- package/scripts/lib/pack-rules.mjs +20 -2
- package/scripts/lib/secret-audit.mjs +510 -0
- package/scripts/render-api-contract-audit.mjs +8 -0
- package/scripts/render-secret-audit.mjs +8 -0
- package/scripts/test-pack.mjs +130 -1
- package/scripts/validate-pack.mjs +8 -2
- package/skills/api-contract-audit/SKILL.md +85 -0
- package/skills/api-contract-audit/adapter-interface.md +16 -0
- package/skills/api-contract-audit/agents/openai.yaml +4 -0
- package/skills/api-contract-audit/checklist.md +7 -0
- package/skills/api-contract-audit/evidence-template.md +13 -0
- package/skills/api-contract-audit/examples.md +20 -0
- package/skills/api-contract-audit/failure-modes.md +5 -0
- package/skills/secret-audit/SKILL.md +57 -0
- package/skills/secret-audit/adapter-interface.md +12 -0
- package/skills/secret-audit/agents/openai.yaml +4 -0
- package/skills/secret-audit/checklist.md +7 -0
- package/skills/secret-audit/evidence-template.md +15 -0
- package/skills/secret-audit/examples.md +27 -0
- package/skills/secret-audit/failure-modes.md +5 -0
- package/tests/fixtures/api-contract-audit/adapter-project/.coding-agent/adapters/api-contract-audit-fixture/adapter.json +53 -0
- package/tests/fixtures/api-contract-audit/adapter-project/.coding-agent/skills.json +23 -0
- package/tests/fixtures/api-contract-audit/adapter-project/README.md +3 -0
- package/tests/fixtures/api-contract-audit/adapter-project/package.json +4 -0
- package/tests/fixtures/api-contract-audit/adapter-project/src/routes.ts +1 -0
- package/tests/fixtures/api-contract-audit/static-project/README.md +3 -0
- package/tests/fixtures/api-contract-audit/static-project/app/api/users/route.ts +7 -0
- package/tests/fixtures/api-contract-audit/static-project/docs/openapi.yaml +10 -0
- package/tests/fixtures/api-contract-audit/static-project/package.json +4 -0
- package/tests/fixtures/api-contract-audit/static-project/schemas/user.schema.ts +4 -0
- package/tests/fixtures/api-contract-audit/static-project/src/client.ts +3 -0
- package/tests/fixtures/secret-audit/adapter-project/.coding-agent/adapters/secret-audit-fixture/adapter.json +53 -0
- package/tests/fixtures/secret-audit/adapter-project/.coding-agent/skills.json +23 -0
- package/tests/fixtures/secret-audit/adapter-project/README.md +3 -0
- package/tests/fixtures/secret-audit/adapter-project/package.json +4 -0
- package/tests/fixtures/secret-audit/adapter-project/src/placeholder.ts +1 -0
- package/tests/fixtures/secret-audit/static-project/.env.example +1 -0
- package/tests/fixtures/secret-audit/static-project/README.md +3 -0
- package/tests/fixtures/secret-audit/static-project/package.json +4 -0
- package/tests/fixtures/secret-audit/static-project/src/config.ts +1 -0
- package/tests/fixtures/triggers/cases.json +25 -1
- package/tests/trigger/README.md +4 -0
- package/work-ledger.md +30 -6
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ADAPTER_MANIFEST_FILENAME,
|
|
8
|
+
readSafeJsonFile,
|
|
9
|
+
} from "./adapter-discovery.mjs";
|
|
10
|
+
import { PILOT_VERSION } from "./pack-rules.mjs";
|
|
11
|
+
import {
|
|
12
|
+
readProjectAdapterDeclaration,
|
|
13
|
+
validateProjectAdapters,
|
|
14
|
+
} from "./project-adapter-installation.mjs";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_CORE_ROOT = path.resolve(
|
|
17
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
18
|
+
"..",
|
|
19
|
+
"..",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const DEFAULT_IGNORED_PATHS = [
|
|
23
|
+
".git",
|
|
24
|
+
".next",
|
|
25
|
+
"node_modules",
|
|
26
|
+
"dist",
|
|
27
|
+
"build",
|
|
28
|
+
"coverage",
|
|
29
|
+
"out",
|
|
30
|
+
"validation-output",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const TEXT_EXTENSIONS = new Set([
|
|
34
|
+
".cjs",
|
|
35
|
+
".cts",
|
|
36
|
+
".js",
|
|
37
|
+
".jsx",
|
|
38
|
+
".json",
|
|
39
|
+
".md",
|
|
40
|
+
".mjs",
|
|
41
|
+
".mts",
|
|
42
|
+
".sql",
|
|
43
|
+
".ts",
|
|
44
|
+
".tsx",
|
|
45
|
+
".txt",
|
|
46
|
+
".yaml",
|
|
47
|
+
".yml",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const REFUSED_BEHAVIOR = [
|
|
51
|
+
"no .env reads",
|
|
52
|
+
"no secret-file reads",
|
|
53
|
+
"no matched value printing",
|
|
54
|
+
"no credential stores",
|
|
55
|
+
"no credential rotation",
|
|
56
|
+
"no API calls",
|
|
57
|
+
"no target project builds",
|
|
58
|
+
"no target project tests",
|
|
59
|
+
"no package installs",
|
|
60
|
+
"no deployments",
|
|
61
|
+
"no migrations",
|
|
62
|
+
"no project writes",
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const NOT_VERIFIED = [
|
|
66
|
+
"ignored secret files such as .env",
|
|
67
|
+
"credential stores and local keychains",
|
|
68
|
+
"runtime-injected secrets",
|
|
69
|
+
"remote repository history",
|
|
70
|
+
"cloud provider secret stores",
|
|
71
|
+
"whether a redacted finding is active or revoked",
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const SECRET_PATTERNS = [
|
|
75
|
+
{
|
|
76
|
+
type: "private-key",
|
|
77
|
+
pattern:
|
|
78
|
+
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: "authorization-header",
|
|
82
|
+
pattern: /Authorization:\s*Bearer\s+[A-Za-z0-9._-]{12,}/gi,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: "github-token",
|
|
86
|
+
pattern: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: "github-token",
|
|
90
|
+
pattern: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/gi,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: "jwt",
|
|
94
|
+
pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
type: "url-credentials",
|
|
98
|
+
pattern: /\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^@\s/]+@[^\s]+/gi,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
type: "secret-assignment",
|
|
102
|
+
pattern:
|
|
103
|
+
/(?:^|\n)[A-Z][A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|PRIVATE_KEY|DATABASE_URL|API_KEY)=[^\s"'`]+/g,
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
function inside(root, candidate) {
|
|
108
|
+
const relative = path.relative(root, candidate);
|
|
109
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function toPosix(relativePath) {
|
|
113
|
+
return relativePath.split(path.sep).join("/");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function safeRelativePath(candidate) {
|
|
117
|
+
return (
|
|
118
|
+
typeof candidate === "string" &&
|
|
119
|
+
candidate.length > 0 &&
|
|
120
|
+
!candidate.startsWith("/") &&
|
|
121
|
+
!candidate.split(/[\\/]+/).includes("..")
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function secretBearingPath(relativePath) {
|
|
126
|
+
const normalized = toPosix(relativePath);
|
|
127
|
+
const basename = path.posix.basename(normalized);
|
|
128
|
+
return (
|
|
129
|
+
basename === ".env" ||
|
|
130
|
+
(basename.startsWith(".env.") && basename !== ".env.example") ||
|
|
131
|
+
basename === ".npmrc" ||
|
|
132
|
+
/\.(?:pem|key|p12|pfx)$/i.test(basename) ||
|
|
133
|
+
/(?:^|\/)(?:secrets?|credentials?|private-key)(?:\/|$)/i.test(normalized)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ignoredBy(relativePath, ignoredPaths) {
|
|
138
|
+
const normalized = toPosix(relativePath);
|
|
139
|
+
return ignoredPaths.some((ignored) => {
|
|
140
|
+
const clean = toPosix(ignored).replace(/\/+$/g, "");
|
|
141
|
+
return normalized === clean || normalized.startsWith(`${clean}/`);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function gitSummary(projectRoot) {
|
|
146
|
+
const summary = {
|
|
147
|
+
root: null,
|
|
148
|
+
branchState: null,
|
|
149
|
+
hasUncommittedChanges: false,
|
|
150
|
+
warnings: [],
|
|
151
|
+
};
|
|
152
|
+
const revParse = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
153
|
+
cwd: projectRoot,
|
|
154
|
+
encoding: "utf8",
|
|
155
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
156
|
+
});
|
|
157
|
+
if (revParse.status === 0) summary.root = revParse.stdout.trim();
|
|
158
|
+
const status = spawnSync("git", ["status", "--short", "--branch"], {
|
|
159
|
+
cwd: projectRoot,
|
|
160
|
+
encoding: "utf8",
|
|
161
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
162
|
+
});
|
|
163
|
+
if (status.status !== 0) {
|
|
164
|
+
summary.warnings.push("git status unavailable");
|
|
165
|
+
return summary;
|
|
166
|
+
}
|
|
167
|
+
const lines = status.stdout.split(/\r?\n/).filter(Boolean);
|
|
168
|
+
summary.branchState = lines[0] ?? null;
|
|
169
|
+
summary.hasUncommittedChanges = lines.length > 1;
|
|
170
|
+
if (summary.hasUncommittedChanges) summary.warnings.push("working tree has local changes; filenames omitted");
|
|
171
|
+
if (/\[(?:ahead|behind|gone|diverged)[^\]]*\]/i.test(summary.branchState ?? "")) {
|
|
172
|
+
summary.warnings.push("branch state indicates remote divergence; revalidate after update");
|
|
173
|
+
}
|
|
174
|
+
return summary;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function adapterContext(projectRootInput, coreRoot) {
|
|
178
|
+
const loaded = readProjectAdapterDeclaration(projectRootInput);
|
|
179
|
+
if (!loaded.ok) {
|
|
180
|
+
if (loaded.codes.length === 1 && loaded.codes[0] === "missing-project-declaration") {
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
present: false,
|
|
184
|
+
enabled: false,
|
|
185
|
+
projectRoot: path.resolve(projectRootInput),
|
|
186
|
+
mode: "none",
|
|
187
|
+
codes: [],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return { ok: false, present: false, enabled: false, status: "failed", codes: loaded.codes };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const validation = validateProjectAdapters(loaded.projectRoot, { coreRoot });
|
|
194
|
+
if (!validation.ok) {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
present: true,
|
|
198
|
+
enabled: false,
|
|
199
|
+
status: "failed",
|
|
200
|
+
codes: validation.codes,
|
|
201
|
+
validation,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!validation.acceptedSkills.includes("secret-audit")) {
|
|
206
|
+
return {
|
|
207
|
+
ok: true,
|
|
208
|
+
present: true,
|
|
209
|
+
enabled: false,
|
|
210
|
+
projectRoot: loaded.projectRoot,
|
|
211
|
+
mode: "adapter-present-secret-audit-not-enabled",
|
|
212
|
+
declarationPath: loaded.declarationPath,
|
|
213
|
+
declaration: loaded.declaration,
|
|
214
|
+
validation,
|
|
215
|
+
codes: ["secret-audit-not-enabled-by-adapter"],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const adapters = [];
|
|
220
|
+
const errors = [];
|
|
221
|
+
const container = path.resolve(loaded.projectRoot, loaded.declaration.adapterRoot);
|
|
222
|
+
if (!inside(loaded.projectRoot, container) || !fs.existsSync(container)) {
|
|
223
|
+
errors.push("adapter-root-not-found");
|
|
224
|
+
} else {
|
|
225
|
+
for (const declaration of loaded.declaration.adapters ?? []) {
|
|
226
|
+
if (!(declaration.skillIds ?? []).includes("secret-audit")) continue;
|
|
227
|
+
const manifestPath = path.join(container, declaration.id, ADAPTER_MANIFEST_FILENAME);
|
|
228
|
+
const record = readSafeJsonFile(manifestPath);
|
|
229
|
+
if (!record.value) {
|
|
230
|
+
errors.push(...record.codes);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
adapters.push({
|
|
234
|
+
declaration,
|
|
235
|
+
manifestPath: path.relative(loaded.projectRoot, manifestPath),
|
|
236
|
+
manifest: record.value,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (errors.length) {
|
|
241
|
+
return { ok: false, present: true, enabled: false, status: "failed", codes: errors, validation };
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
ok: true,
|
|
245
|
+
present: true,
|
|
246
|
+
enabled: true,
|
|
247
|
+
projectRoot: loaded.projectRoot,
|
|
248
|
+
mode: "adapter-enabled",
|
|
249
|
+
declarationPath: loaded.declarationPath,
|
|
250
|
+
declaration: loaded.declaration,
|
|
251
|
+
validation,
|
|
252
|
+
adapters,
|
|
253
|
+
codes: [],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function scopeFromAdapter(context) {
|
|
258
|
+
const safeReadPaths = new Set();
|
|
259
|
+
const ignoredPaths = new Set(DEFAULT_IGNORED_PATHS);
|
|
260
|
+
for (const adapter of context.adapters ?? []) {
|
|
261
|
+
for (const candidate of adapter.manifest.extensions?.safeReadPaths ?? []) {
|
|
262
|
+
if (safeRelativePath(candidate)) safeReadPaths.add(candidate);
|
|
263
|
+
}
|
|
264
|
+
for (const candidate of adapter.manifest.extensions?.ignoredPaths ?? []) {
|
|
265
|
+
if (safeRelativePath(candidate)) ignoredPaths.add(candidate);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
scopePaths: safeReadPaths.size ? [...safeReadPaths].sort() : ["."],
|
|
270
|
+
ignoredPaths: [...ignoredPaths].sort(),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function candidateFile(relativePath) {
|
|
275
|
+
const basename = path.posix.basename(toPosix(relativePath));
|
|
276
|
+
if (basename === ".env.example") return true;
|
|
277
|
+
return TEXT_EXTENSIONS.has(path.extname(basename));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function collectFiles(projectRoot, scopePaths, ignoredPaths) {
|
|
281
|
+
const files = [];
|
|
282
|
+
const skipped = [];
|
|
283
|
+
for (const scopePath of scopePaths) {
|
|
284
|
+
if (!safeRelativePath(scopePath) && scopePath !== ".") {
|
|
285
|
+
skipped.push({ path: scopePath, reason: "unsafe scope path" });
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const absolute = path.resolve(projectRoot, scopePath);
|
|
289
|
+
if (!inside(projectRoot, absolute)) {
|
|
290
|
+
skipped.push({ path: scopePath, reason: "scope escapes project root" });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (!fs.existsSync(absolute)) {
|
|
294
|
+
skipped.push({ path: scopePath, reason: "scope path not found" });
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
walkPath(projectRoot, absolute, ignoredPaths, files, skipped);
|
|
298
|
+
}
|
|
299
|
+
return { files: [...new Set(files)].sort(), skipped };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function walkPath(projectRoot, absolute, ignoredPaths, files, skipped) {
|
|
303
|
+
const relative = toPosix(path.relative(projectRoot, absolute)) || ".";
|
|
304
|
+
if (relative !== "." && ignoredBy(relative, ignoredPaths)) {
|
|
305
|
+
skipped.push({ path: relative, reason: "ignored path" });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (relative !== "." && secretBearingPath(relative)) {
|
|
309
|
+
skipped.push({ path: relative, reason: "secret-bearing path excluded" });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const stat = fs.lstatSync(absolute);
|
|
313
|
+
if (stat.isSymbolicLink()) {
|
|
314
|
+
skipped.push({ path: relative, reason: "symbolic link skipped" });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (stat.isDirectory()) {
|
|
318
|
+
for (const entry of fs.readdirSync(absolute)) {
|
|
319
|
+
walkPath(projectRoot, path.join(absolute, entry), ignoredPaths, files, skipped);
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (!stat.isFile()) return;
|
|
324
|
+
if (stat.size > 512_000) {
|
|
325
|
+
skipped.push({ path: relative, reason: "file larger than bounded read limit" });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (!candidateFile(relative)) {
|
|
329
|
+
skipped.push({ path: relative, reason: "not a secret-audit candidate file" });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
files.push(relative);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function scanSecrets(projectRoot, files) {
|
|
336
|
+
const findings = [];
|
|
337
|
+
const warnings = [];
|
|
338
|
+
for (const relative of files) {
|
|
339
|
+
let text = "";
|
|
340
|
+
try {
|
|
341
|
+
text = fs.readFileSync(path.join(projectRoot, relative), "utf8");
|
|
342
|
+
} catch {
|
|
343
|
+
warnings.push(`could not read ${relative}`);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
for (const secretPattern of SECRET_PATTERNS) {
|
|
347
|
+
secretPattern.pattern.lastIndex = 0;
|
|
348
|
+
let count = 0;
|
|
349
|
+
for (const _match of text.matchAll(secretPattern.pattern)) count += 1;
|
|
350
|
+
if (count > 0) findings.push({ path: relative, type: secretPattern.type, count });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return { findings: findings.sort((a, b) => `${a.path}:${a.type}`.localeCompare(`${b.path}:${b.type}`)), warnings };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function buildSecretAuditReport(projectRootInput, options = {}) {
|
|
357
|
+
const coreRoot = options.coreRoot ?? DEFAULT_CORE_ROOT;
|
|
358
|
+
const context = adapterContext(projectRootInput, coreRoot);
|
|
359
|
+
if (!context.ok) {
|
|
360
|
+
return {
|
|
361
|
+
status: "failed",
|
|
362
|
+
coreVersion: PILOT_VERSION,
|
|
363
|
+
projectRoot: path.resolve(projectRootInput ?? "."),
|
|
364
|
+
adapter: context,
|
|
365
|
+
git: gitSummary(path.resolve(projectRootInput ?? ".")),
|
|
366
|
+
scopePaths: [],
|
|
367
|
+
ignoredPaths: DEFAULT_IGNORED_PATHS,
|
|
368
|
+
filesScanned: [],
|
|
369
|
+
findings: [],
|
|
370
|
+
skipped: [],
|
|
371
|
+
warnings: context.codes ?? [],
|
|
372
|
+
notVerified: NOT_VERIFIED,
|
|
373
|
+
refusedBehavior: REFUSED_BEHAVIOR,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const projectRoot = context.projectRoot;
|
|
378
|
+
const git = gitSummary(projectRoot);
|
|
379
|
+
if (context.present && !context.enabled) {
|
|
380
|
+
return {
|
|
381
|
+
status: "partial",
|
|
382
|
+
coreVersion: PILOT_VERSION,
|
|
383
|
+
projectRoot,
|
|
384
|
+
adapter: context,
|
|
385
|
+
git,
|
|
386
|
+
scopePaths: [],
|
|
387
|
+
ignoredPaths: DEFAULT_IGNORED_PATHS,
|
|
388
|
+
filesScanned: [],
|
|
389
|
+
findings: [],
|
|
390
|
+
skipped: [{ path: ".", reason: "project adapter is present but does not enable secret-audit" }],
|
|
391
|
+
warnings: [
|
|
392
|
+
"secret-audit is not enabled by the project adapter; target files were not read",
|
|
393
|
+
...git.warnings,
|
|
394
|
+
],
|
|
395
|
+
notVerified: NOT_VERIFIED,
|
|
396
|
+
refusedBehavior: REFUSED_BEHAVIOR,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const scope = context.enabled
|
|
401
|
+
? scopeFromAdapter(context)
|
|
402
|
+
: { scopePaths: ["."], ignoredPaths: DEFAULT_IGNORED_PATHS };
|
|
403
|
+
const collected = collectFiles(projectRoot, scope.scopePaths, scope.ignoredPaths);
|
|
404
|
+
const scanned = scanSecrets(projectRoot, collected.files);
|
|
405
|
+
return {
|
|
406
|
+
status: "complete",
|
|
407
|
+
coreVersion: PILOT_VERSION,
|
|
408
|
+
projectRoot,
|
|
409
|
+
adapter: context,
|
|
410
|
+
git,
|
|
411
|
+
scopePaths: scope.scopePaths,
|
|
412
|
+
ignoredPaths: scope.ignoredPaths,
|
|
413
|
+
filesScanned: collected.files,
|
|
414
|
+
findings: scanned.findings,
|
|
415
|
+
skipped: collected.skipped,
|
|
416
|
+
warnings: [
|
|
417
|
+
...git.warnings,
|
|
418
|
+
...scanned.warnings,
|
|
419
|
+
...(context.enabled ? ["secret-audit used adapter-declared safe read paths only"] : []),
|
|
420
|
+
...(context.present ? [] : ["no project adapter declaration found; secret-audit used generic bounded static scan"]),
|
|
421
|
+
],
|
|
422
|
+
notVerified: NOT_VERIFIED,
|
|
423
|
+
refusedBehavior: REFUSED_BEHAVIOR,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function renderSecretAuditReport(report) {
|
|
428
|
+
const totalFindings = report.findings.reduce((sum, finding) => sum + finding.count, 0);
|
|
429
|
+
const lines = [
|
|
430
|
+
"# Secret Audit Report",
|
|
431
|
+
"",
|
|
432
|
+
`Status: ${report.status}`,
|
|
433
|
+
`Core version: ${report.coreVersion}`,
|
|
434
|
+
`Project root: ${report.projectRoot}`,
|
|
435
|
+
"",
|
|
436
|
+
"## Git State",
|
|
437
|
+
`- Git root: ${report.git.root ?? "not detected"}`,
|
|
438
|
+
`- Branch state: ${report.git.branchState ?? "not detected"}`,
|
|
439
|
+
`- Local changes: ${report.git.hasUncommittedChanges ? "detected" : "not detected"}`,
|
|
440
|
+
"",
|
|
441
|
+
"## Adapter Scope",
|
|
442
|
+
`- Adapter present: ${report.adapter.present ? "yes" : "no"}`,
|
|
443
|
+
`- Secret-audit enabled: ${report.adapter.enabled ? "yes" : "no"}`,
|
|
444
|
+
`- Mode: ${report.adapter.mode}`,
|
|
445
|
+
"",
|
|
446
|
+
"## Scope Paths",
|
|
447
|
+
...(report.scopePaths.length ? report.scopePaths.map((item) => `- ${item}`) : ["- none"]),
|
|
448
|
+
"",
|
|
449
|
+
"## Ignored Paths",
|
|
450
|
+
...report.ignoredPaths.map((item) => `- ${item}`),
|
|
451
|
+
"",
|
|
452
|
+
"## Summary",
|
|
453
|
+
`- Static files scanned: ${report.filesScanned.length}`,
|
|
454
|
+
`- Secret-like findings: ${totalFindings}`,
|
|
455
|
+
`- Files with findings: ${new Set(report.findings.map((finding) => finding.path)).size}`,
|
|
456
|
+
`- Skipped items: ${report.skipped.length}`,
|
|
457
|
+
"",
|
|
458
|
+
"## Findings",
|
|
459
|
+
];
|
|
460
|
+
if (report.findings.length) {
|
|
461
|
+
for (const finding of report.findings) {
|
|
462
|
+
lines.push(`- ${finding.path}: ${finding.count} ${finding.type} match(es); values omitted`);
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
lines.push("- none found");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
lines.push("", "## Skipped");
|
|
469
|
+
if (report.skipped.length) {
|
|
470
|
+
for (const skipped of report.skipped.slice(0, 40)) {
|
|
471
|
+
lines.push(`- ${skipped.path}: ${skipped.reason}`);
|
|
472
|
+
}
|
|
473
|
+
if (report.skipped.length > 40) lines.push(`- ${report.skipped.length - 40} additional skipped items omitted`);
|
|
474
|
+
} else {
|
|
475
|
+
lines.push("- none");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
lines.push("", "## Not Verified");
|
|
479
|
+
for (const item of report.notVerified) lines.push(`- ${item}`);
|
|
480
|
+
lines.push("", "## Warnings");
|
|
481
|
+
if (report.warnings.length) {
|
|
482
|
+
for (const warning of report.warnings) lines.push(`- ${warning}`);
|
|
483
|
+
} else {
|
|
484
|
+
lines.push("- none");
|
|
485
|
+
}
|
|
486
|
+
lines.push("", "## Refused Behavior");
|
|
487
|
+
for (const item of report.refusedBehavior) lines.push(`- ${item}`);
|
|
488
|
+
lines.push(
|
|
489
|
+
"",
|
|
490
|
+
"No .env, secret-file, credential-store, API, build, test, deploy, migration, package-install, value-printing, rotation, or project-write behavior was performed.",
|
|
491
|
+
);
|
|
492
|
+
return lines.join("\n");
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function secretAuditCliResult(projectRootInput, options = {}) {
|
|
496
|
+
if (!projectRootInput) {
|
|
497
|
+
return {
|
|
498
|
+
exitCode: 2,
|
|
499
|
+
stream: "stderr",
|
|
500
|
+
lines: ["usage: node scripts/render-secret-audit.mjs <project-root>"],
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const report = buildSecretAuditReport(projectRootInput, options);
|
|
504
|
+
return {
|
|
505
|
+
exitCode: report.status === "failed" ? 1 : 0,
|
|
506
|
+
stream: report.status === "failed" ? "stderr" : "stdout",
|
|
507
|
+
lines: renderSecretAuditReport(report).split("\n"),
|
|
508
|
+
report,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { apiContractAuditCliResult } from "./lib/api-contract-audit.mjs";
|
|
2
|
+
|
|
3
|
+
const outcome = apiContractAuditCliResult(process.argv[2]);
|
|
4
|
+
for (const line of outcome.lines) {
|
|
5
|
+
if (outcome.stream === "stdout") console.log(line);
|
|
6
|
+
else console.error(line);
|
|
7
|
+
}
|
|
8
|
+
process.exitCode = outcome.exitCode;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { secretAuditCliResult } from "./lib/secret-audit.mjs";
|
|
2
|
+
|
|
3
|
+
const outcome = secretAuditCliResult(process.argv[2]);
|
|
4
|
+
for (const line of outcome.lines) {
|
|
5
|
+
if (outcome.stream === "stdout") console.log(line);
|
|
6
|
+
else console.error(line);
|
|
7
|
+
}
|
|
8
|
+
process.exitCode = outcome.exitCode;
|
package/scripts/test-pack.mjs
CHANGED
|
@@ -52,6 +52,16 @@ import {
|
|
|
52
52
|
envAuditCliResult,
|
|
53
53
|
renderEnvAuditReport,
|
|
54
54
|
} from "./lib/env-audit.mjs";
|
|
55
|
+
import {
|
|
56
|
+
buildSecretAuditReport,
|
|
57
|
+
renderSecretAuditReport,
|
|
58
|
+
secretAuditCliResult,
|
|
59
|
+
} from "./lib/secret-audit.mjs";
|
|
60
|
+
import {
|
|
61
|
+
apiContractAuditCliResult,
|
|
62
|
+
buildApiContractAuditReport,
|
|
63
|
+
renderApiContractAuditReport,
|
|
64
|
+
} from "./lib/api-contract-audit.mjs";
|
|
55
65
|
import {
|
|
56
66
|
adapterUpgradeCliResult,
|
|
57
67
|
checkAdapterUpgrade,
|
|
@@ -268,6 +278,8 @@ test("local CLI maps approved commands to existing safe scripts", () => {
|
|
|
268
278
|
assert.ok(cliText.includes("scripts/render-adapter-repo-map.mjs"));
|
|
269
279
|
assert.ok(cliText.includes("scripts/render-route-trace.mjs"));
|
|
270
280
|
assert.ok(cliText.includes("scripts/render-env-audit.mjs"));
|
|
281
|
+
assert.ok(cliText.includes("scripts/render-secret-audit.mjs"));
|
|
282
|
+
assert.ok(cliText.includes("scripts/render-api-contract-audit.mjs"));
|
|
271
283
|
assert.ok(cliText.includes("scripts/validate-adapters.mjs"));
|
|
272
284
|
assert.ok(!cliText.includes(".env"));
|
|
273
285
|
|
|
@@ -297,6 +309,14 @@ test("local CLI maps approved commands to existing safe scripts", () => {
|
|
|
297
309
|
["env-audit", path.join(fixtureRoot, "env-audit", "static-project")],
|
|
298
310
|
/# Env Audit Report/,
|
|
299
311
|
],
|
|
312
|
+
[
|
|
313
|
+
["secret-audit", path.join(fixtureRoot, "secret-audit", "static-project")],
|
|
314
|
+
/# Secret Audit Report/,
|
|
315
|
+
],
|
|
316
|
+
[
|
|
317
|
+
["api-contract-audit", path.join(fixtureRoot, "api-contract-audit", "static-project")],
|
|
318
|
+
/# API Contract Audit Report/,
|
|
319
|
+
],
|
|
300
320
|
];
|
|
301
321
|
|
|
302
322
|
for (const [args, expected] of commands) {
|
|
@@ -321,7 +341,7 @@ test("local CLI maps approved commands to existing safe scripts", () => {
|
|
|
321
341
|
test("npm package metadata is public-ready and dependency-free", () => {
|
|
322
342
|
const packageJson = readJson("package.json");
|
|
323
343
|
assert.equal(packageJson.name, "coding-agent-skills");
|
|
324
|
-
assert.equal(packageJson.version, "0.2.
|
|
344
|
+
assert.equal(packageJson.version, "0.2.12");
|
|
325
345
|
assert.equal(
|
|
326
346
|
packageJson.description,
|
|
327
347
|
"Evidence-first, read-only coding-agent skills and project adapter tooling.",
|
|
@@ -335,6 +355,8 @@ test("npm package metadata is public-ready and dependency-free", () => {
|
|
|
335
355
|
"repo-map",
|
|
336
356
|
"route-trace",
|
|
337
357
|
"env-audit",
|
|
358
|
+
"secret-audit",
|
|
359
|
+
"api-contract-audit",
|
|
338
360
|
"project-adapters",
|
|
339
361
|
"code-validation",
|
|
340
362
|
"cli",
|
|
@@ -521,6 +543,113 @@ test("env-audit does not broaden a repo-map-only project adapter", () => {
|
|
|
521
543
|
assert.match(renderEnvAuditReport(result), /env-audit is not enabled/);
|
|
522
544
|
});
|
|
523
545
|
|
|
546
|
+
test("secret-audit reports high-confidence findings without printing values", () => {
|
|
547
|
+
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "secret-audit-fixture-"));
|
|
548
|
+
fs.cpSync(path.join(root, "tests", "fixtures", "secret-audit", "static-project"), temporary, {
|
|
549
|
+
recursive: true,
|
|
550
|
+
});
|
|
551
|
+
const syntheticSecret = ["github", "_pat_", "A".repeat(24)].join("");
|
|
552
|
+
fs.writeFileSync(
|
|
553
|
+
path.join(temporary, "src", "leak.ts"),
|
|
554
|
+
`export const candidate = "${syntheticSecret}";\n`,
|
|
555
|
+
);
|
|
556
|
+
fs.writeFileSync(path.join(temporary, ".env"), `DO_NOT_READ=${syntheticSecret}\n`);
|
|
557
|
+
|
|
558
|
+
const result = buildSecretAuditReport(temporary, { coreRoot: root });
|
|
559
|
+
const rendered = renderSecretAuditReport(result);
|
|
560
|
+
|
|
561
|
+
assert.equal(result.status, "complete");
|
|
562
|
+
assert.ok(result.filesScanned.includes("src/leak.ts"));
|
|
563
|
+
assert.ok(!result.filesScanned.includes(".env"));
|
|
564
|
+
assert.ok(result.skipped.some((item) => item.path === ".env"));
|
|
565
|
+
assert.ok(result.findings.some((finding) => finding.path === "src/leak.ts"));
|
|
566
|
+
assert.ok(result.findings.some((finding) => finding.type === "github-token"));
|
|
567
|
+
assert.match(rendered, /src\/leak\.ts/);
|
|
568
|
+
assert.match(rendered, /values omitted/);
|
|
569
|
+
assert.doesNotMatch(rendered, new RegExp(syntheticSecret));
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("secret-audit respects adapter-declared scope", () => {
|
|
573
|
+
const result = buildSecretAuditReport(
|
|
574
|
+
path.join(root, "tests", "fixtures", "secret-audit", "adapter-project"),
|
|
575
|
+
{ coreRoot: root },
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
assert.equal(result.status, "complete");
|
|
579
|
+
assert.equal(result.adapter.enabled, true);
|
|
580
|
+
assert.deepEqual(result.scopePaths, ["src"]);
|
|
581
|
+
assert.deepEqual(result.filesScanned, ["src/placeholder.ts"]);
|
|
582
|
+
assert.ok(result.warnings.includes("secret-audit used adapter-declared safe read paths only"));
|
|
583
|
+
const cli = secretAuditCliResult(
|
|
584
|
+
path.join(root, "tests", "fixtures", "secret-audit", "adapter-project"),
|
|
585
|
+
{ coreRoot: root },
|
|
586
|
+
);
|
|
587
|
+
assert.equal(cli.exitCode, 0);
|
|
588
|
+
assert.match(cli.lines.join("\n"), /Secret-audit enabled: yes/);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("secret-audit does not broaden a repo-map-only project adapter", () => {
|
|
592
|
+
const result = buildSecretAuditReport(
|
|
593
|
+
path.join(root, "tests", "fixtures", "project-adapter-installation", "valid-exact-pin"),
|
|
594
|
+
{ coreRoot: root },
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
assert.equal(result.status, "partial");
|
|
598
|
+
assert.equal(result.filesScanned.length, 0);
|
|
599
|
+
assert.equal(result.findings.length, 0);
|
|
600
|
+
assert.match(renderSecretAuditReport(result), /secret-audit is not enabled/);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
test("api-contract-audit maps static contract surfaces without runtime behavior", () => {
|
|
604
|
+
const result = buildApiContractAuditReport(
|
|
605
|
+
path.join(root, "tests", "fixtures", "api-contract-audit", "static-project"),
|
|
606
|
+
{ coreRoot: root },
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
assert.equal(result.status, "complete");
|
|
610
|
+
assert.ok(result.contractFiles.some((record) => record.path === "docs/openapi.yaml"));
|
|
611
|
+
assert.ok(
|
|
612
|
+
result.endpointDeclarations.some(
|
|
613
|
+
(record) => record.route === "/api/users" && record.method === "GET",
|
|
614
|
+
),
|
|
615
|
+
);
|
|
616
|
+
assert.ok(result.clientCalls.some((record) => record.target === "/api/users"));
|
|
617
|
+
assert.ok(result.schemaFiles.some((record) => record.path === "schemas/user.schema.ts"));
|
|
618
|
+
assert.match(renderApiContractAuditReport(result), /No target project build/);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test("api-contract-audit respects adapter-declared scope", () => {
|
|
622
|
+
const result = buildApiContractAuditReport(
|
|
623
|
+
path.join(root, "tests", "fixtures", "api-contract-audit", "adapter-project"),
|
|
624
|
+
{ coreRoot: root },
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
assert.equal(result.status, "complete");
|
|
628
|
+
assert.equal(result.adapter.enabled, true);
|
|
629
|
+
assert.deepEqual(result.scopePaths, ["src"]);
|
|
630
|
+
assert.deepEqual(result.filesScanned, ["src/routes.ts"]);
|
|
631
|
+
assert.ok(result.endpointDeclarations.some((record) => record.route === "/api/adapter-items"));
|
|
632
|
+
assert.ok(result.warnings.includes("api-contract-audit used adapter-declared safe read paths only"));
|
|
633
|
+
const cli = apiContractAuditCliResult(
|
|
634
|
+
path.join(root, "tests", "fixtures", "api-contract-audit", "adapter-project"),
|
|
635
|
+
{ coreRoot: root },
|
|
636
|
+
);
|
|
637
|
+
assert.equal(cli.exitCode, 0);
|
|
638
|
+
assert.match(cli.lines.join("\n"), /API-contract-audit enabled: yes/);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test("api-contract-audit does not broaden a repo-map-only project adapter", () => {
|
|
642
|
+
const result = buildApiContractAuditReport(
|
|
643
|
+
path.join(root, "tests", "fixtures", "project-adapter-installation", "valid-exact-pin"),
|
|
644
|
+
{ coreRoot: root },
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
assert.equal(result.status, "partial");
|
|
648
|
+
assert.equal(result.filesScanned.length, 0);
|
|
649
|
+
assert.equal(result.endpointDeclarations.length, 0);
|
|
650
|
+
assert.match(renderApiContractAuditReport(result), /api-contract-audit is not enabled/);
|
|
651
|
+
});
|
|
652
|
+
|
|
524
653
|
test("validate-pack accepts installed package trees without source-only gitignore", () => {
|
|
525
654
|
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "installed-package-"));
|
|
526
655
|
const installedRoot = path.join(temporaryRoot, "coding-agent-skills");
|