shipready 1.3.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -8
- package/dist/checks/packageJson.js +11 -0
- package/dist/checks/staged.js +55 -0
- package/dist/checks/todos.js +4 -0
- package/dist/cli.js +88 -16
- package/dist/fixers/agentFiles.js +6 -3
- package/dist/fixers/envExample.js +11 -3
- package/dist/fixers/gitignore.js +10 -5
- package/dist/scanner.js +3 -1
- package/dist/utils/files.js +31 -8
- package/dist/utils/framework.js +141 -31
- package/dist/utils/report.js +4 -1
- package/dist/utils/sarif.js +85 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://github.com/formalness/shipready/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/shipready)
|
|
7
|
+
[](https://github.com/formalness/shipready/tree/main/tests)
|
|
7
8
|
[](./LICENSE)
|
|
8
9
|
[](https://nodejs.org)
|
|
9
10
|
|
|
@@ -37,9 +38,10 @@ shipready check
|
|
|
37
38
|
## Usage
|
|
38
39
|
|
|
39
40
|
```bash
|
|
40
|
-
shipready check
|
|
41
|
-
shipready
|
|
42
|
-
shipready
|
|
41
|
+
shipready check [path] # scan a project (current dir by default)
|
|
42
|
+
shipready staged [path] # fast secrets-only scan of staged files (pre-commit)
|
|
43
|
+
shipready init [path] # generate AI-agent instruction files
|
|
44
|
+
shipready fix [path] # apply safe automatic fixes (--dry-run to preview)
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
## Commands
|
|
@@ -52,6 +54,7 @@ Scans the project and prints a report with a 0-100 score.
|
|
|
52
54
|
| --- | --- |
|
|
53
55
|
| `-v, --verbose` | show file and line locations for every finding |
|
|
54
56
|
| `--json` | output the raw structured report as JSON (great for CI) |
|
|
57
|
+
| `--sarif` | output SARIF 2.1.0 for **GitHub code scanning** — findings become native PR alerts |
|
|
55
58
|
| `--fix` | apply safe fixes, re-scan, and show the score before/after |
|
|
56
59
|
| `--history` | also scan the **full git history** (all branches) for secrets that were committed and later removed |
|
|
57
60
|
| `--verify` | check detected keys against provider APIs to see if they are **live right now** |
|
|
@@ -74,6 +77,27 @@ For 12+ providers (OpenAI, Anthropic, GitHub, GitLab, Stripe, SendGrid, Google,
|
|
|
74
77
|
|
|
75
78
|
Verification requests contain only the key itself, go directly to the provider's official API host, and never mutate remote state. Nothing is ever sent to any third party.
|
|
76
79
|
|
|
80
|
+
### `shipready staged [path]`
|
|
81
|
+
|
|
82
|
+
Fast, secrets-only scan of the files **staged for commit** — built for pre-commit hooks. It reads content from the git index (`git show :file`), so partially staged files are checked exactly as they would be committed, not as they sit on disk.
|
|
83
|
+
|
|
84
|
+
- High-confidence secrets **block the commit** (exit code 1)
|
|
85
|
+
- Medium-confidence findings warn but do not block
|
|
86
|
+
- TODO/console.log are deliberately not checked — hooks must be fast and only stop real dangers
|
|
87
|
+
|
|
88
|
+
Hook setup with [husky](https://typicode.github.io/husky/):
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npx husky init
|
|
92
|
+
echo "npx shipready staged" > .husky/pre-commit
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Or plain git:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
echo 'npx shipready staged' > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
|
|
99
|
+
```
|
|
100
|
+
|
|
77
101
|
### `shipready init [path]`
|
|
78
102
|
|
|
79
103
|
Generates AI-agent instruction files based on your detected framework, package manager, and scripts:
|
|
@@ -94,6 +118,21 @@ Applies safe automatic fixes:
|
|
|
94
118
|
|
|
95
119
|
It never deletes user code and never overwrites files without `--force`. A summary of changed files is printed at the end.
|
|
96
120
|
|
|
121
|
+
Pass `--dry-run` to preview exactly what would change — nothing is written to disk:
|
|
122
|
+
|
|
123
|
+
```txt
|
|
124
|
+
shipready fix --dry-run
|
|
125
|
+
|
|
126
|
+
+ would create .env.example
|
|
127
|
+
| # Environment variables used by this project.
|
|
128
|
+
| API_URL=
|
|
129
|
+
+ would create .gitignore
|
|
130
|
+
| .env
|
|
131
|
+
| node_modules
|
|
132
|
+
|
|
133
|
+
2 files would change. Run without --dry-run to apply.
|
|
134
|
+
```
|
|
135
|
+
|
|
97
136
|
## Example output
|
|
98
137
|
|
|
99
138
|
```txt
|
|
@@ -194,7 +233,14 @@ If shipready flags a line you know is safe (a demo value, an already-revoked key
|
|
|
194
233
|
const DEMO_TOKEN = "ghp_thisIsADocumentationExample000000"; // shipready-ignore
|
|
195
234
|
```
|
|
196
235
|
|
|
197
|
-
The
|
|
236
|
+
The marker works for **all checks** — secrets, `console.log`, `TODO`/`FIXME`, and `debugger` alike:
|
|
237
|
+
|
|
238
|
+
```js
|
|
239
|
+
console.log("startup banner"); // shipready-ignore
|
|
240
|
+
// TODO: legacy, tracked in JIRA-123 - shipready-ignore
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
For project-wide exceptions, prefer `secretAllowlist` in the config (see below) so the suppression is reviewable in one place.
|
|
198
244
|
|
|
199
245
|
## Configuration
|
|
200
246
|
|
|
@@ -251,6 +297,33 @@ Example with options:
|
|
|
251
297
|
version: "1.0.3"
|
|
252
298
|
```
|
|
253
299
|
|
|
300
|
+
### GitHub code scanning (SARIF)
|
|
301
|
+
|
|
302
|
+
Turn shipready findings into **native code scanning alerts** on pull requests:
|
|
303
|
+
|
|
304
|
+
```yaml
|
|
305
|
+
# .github/workflows/shipready-sarif.yml
|
|
306
|
+
name: shipready code scanning
|
|
307
|
+
on: [push, pull_request]
|
|
308
|
+
permissions:
|
|
309
|
+
security-events: write
|
|
310
|
+
jobs:
|
|
311
|
+
scan:
|
|
312
|
+
runs-on: ubuntu-latest
|
|
313
|
+
steps:
|
|
314
|
+
- uses: actions/checkout@v4
|
|
315
|
+
- run: npx shipready check --sarif > shipready.sarif || true
|
|
316
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
317
|
+
with:
|
|
318
|
+
sarif_file: shipready.sarif
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Findings appear in the repository's **Security → Code scanning** tab and as inline PR annotations, with stable fingerprints so alerts track across pushes.
|
|
322
|
+
|
|
323
|
+
### Pre-commit hook
|
|
324
|
+
|
|
325
|
+
Catch secrets **before** they enter history at all — see [`shipready staged`](#shipready-staged-path) above.
|
|
326
|
+
|
|
254
327
|
### Manual setup
|
|
255
328
|
|
|
256
329
|
`shipready check` exits with code `1` when errors are found, so it works in any CI:
|
|
@@ -259,7 +332,7 @@ Example with options:
|
|
|
259
332
|
- run: npx shipready check --verbose
|
|
260
333
|
```
|
|
261
334
|
|
|
262
|
-
Use `--json` to feed the report into other tooling.
|
|
335
|
+
Use `--json` to feed the report into other tooling, or `--sarif` for anything that speaks SARIF.
|
|
263
336
|
|
|
264
337
|
## Scoring
|
|
265
338
|
|
|
@@ -292,10 +365,13 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for project structure and how to add ne
|
|
|
292
365
|
|
|
293
366
|
## Roadmap
|
|
294
367
|
|
|
368
|
+
- [x] `shipready staged` for pre-commit hooks
|
|
369
|
+
- [x] SARIF report output for GitHub code scanning
|
|
370
|
+
- [x] Git history scanning (`--history`)
|
|
371
|
+
- [x] Live key verification (`--verify`)
|
|
372
|
+
- [ ] Benchmark against gitleaks/trufflehog with published numbers
|
|
373
|
+
- [ ] Framework-aware scoring (Next.js vs Vite vs Python have different needs)
|
|
295
374
|
- [ ] Optional AI-powered fix suggestions (bring your own key)
|
|
296
|
-
- [ ] `shipready check --staged` for pre-commit hooks
|
|
297
|
-
- [ ] Markdown/SARIF report output for GitHub code scanning
|
|
298
|
-
- [ ] More frameworks and secret providers
|
|
299
375
|
|
|
300
376
|
## License
|
|
301
377
|
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
+
import { isNodeEcosystem } from "../utils/framework.js";
|
|
1
2
|
const IMPORTANT_SCRIPTS = ["dev", "build", "test", "lint"];
|
|
2
3
|
/** Checks package.json existence and important scripts. */
|
|
3
4
|
export function checkPackageJson(project) {
|
|
4
5
|
const findings = [];
|
|
5
6
|
if (!project.hasPackageJson) {
|
|
7
|
+
// Static sites, Python, Go, Rust, etc. don't need a package.json -
|
|
8
|
+
// penalizing them for it would be unfair noise.
|
|
9
|
+
if (!isNodeEcosystem(project.framework)) {
|
|
10
|
+
findings.push({
|
|
11
|
+
severity: "info",
|
|
12
|
+
rule: "package-json.not-applicable",
|
|
13
|
+
message: `package.json not required for a ${project.framework} project`,
|
|
14
|
+
});
|
|
15
|
+
return { name: "package.json", findings };
|
|
16
|
+
}
|
|
6
17
|
findings.push({
|
|
7
18
|
severity: "error",
|
|
8
19
|
rule: "package-json.missing",
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { scanContentForSecrets } from "./secrets.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pre-commit mode: scans only the files staged in the git index.
|
|
5
|
+
* Reads content via `git show :<file>` so partially staged files are
|
|
6
|
+
* checked exactly as they would be committed, not as they sit on disk.
|
|
7
|
+
* Secrets-only by design - a pre-commit hook must be fast and only
|
|
8
|
+
* block on real dangers, not TODO comments.
|
|
9
|
+
*/
|
|
10
|
+
/** Extensions and paths we skip in staged mode (binary/vendor noise). */
|
|
11
|
+
const STAGED_SKIP_RE = /(^|\/)(node_modules|vendor|dist|build|\.next|out|coverage)(\/|$)|\.(lock|min\.js|min\.css|map|png|jpg|jpeg|gif|webp|svg|ico|pdf|zip|gz|woff2?|ttf|eot|mp3|mp4)$|(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?)$/i;
|
|
12
|
+
/** Lists files staged for commit (added/copied/modified/renamed). */
|
|
13
|
+
export function listStagedFiles(root) {
|
|
14
|
+
try {
|
|
15
|
+
const out = execFileSync("git", ["diff", "--cached", "--name-only", "--diff-filter=ACMR", "-z"], { cwd: root, encoding: "utf8" });
|
|
16
|
+
return out.split("\0").filter((f) => f.length > 0 && !STAGED_SKIP_RE.test(f));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Reads a file's staged (index) content, not the working-tree version. */
|
|
23
|
+
export function readStagedContent(root, file) {
|
|
24
|
+
try {
|
|
25
|
+
const buf = execFileSync("git", ["show", `:${file}`], {
|
|
26
|
+
cwd: root,
|
|
27
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
28
|
+
});
|
|
29
|
+
// Binary sniff: NUL byte in the first 512 bytes.
|
|
30
|
+
const head = buf.subarray(0, 512);
|
|
31
|
+
for (const byte of head) {
|
|
32
|
+
if (byte === 0)
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return buf.toString("utf8");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Scans all staged files for secrets. */
|
|
42
|
+
export function scanStaged(root, allowlist = []) {
|
|
43
|
+
const files = listStagedFiles(root);
|
|
44
|
+
const secrets = [];
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
// .env files are expected to hold real values; committing one at all is
|
|
47
|
+
// the problem, and the gitignore check covers that. But since it IS
|
|
48
|
+
// being committed here, flag everything inside it too.
|
|
49
|
+
const content = readStagedContent(root, file);
|
|
50
|
+
if (content === null)
|
|
51
|
+
continue;
|
|
52
|
+
secrets.push(...scanContentForSecrets(content, file, allowlist));
|
|
53
|
+
}
|
|
54
|
+
return { files, secrets };
|
|
55
|
+
}
|
package/dist/checks/todos.js
CHANGED
|
@@ -13,6 +13,10 @@ export function scanContentForTodos(content, file) {
|
|
|
13
13
|
const lines = content.split("\n");
|
|
14
14
|
for (let i = 0; i < lines.length; i++) {
|
|
15
15
|
const line = lines[i];
|
|
16
|
+
// Same suppression marker the secret scanner honors - one convention
|
|
17
|
+
// for all checks, so intentional console.log/TODO lines stay quiet.
|
|
18
|
+
if (line.includes("shipready-ignore"))
|
|
19
|
+
continue;
|
|
16
20
|
const marker = line.match(MARKER_RE);
|
|
17
21
|
if (marker) {
|
|
18
22
|
found.push({ kind: "marker", label: marker[1], file, line: i + 1 });
|
package/dist/cli.js
CHANGED
|
@@ -9,17 +9,27 @@ import { fixAgentFiles } from "./fixers/agentFiles.js";
|
|
|
9
9
|
import { fixEnvExample } from "./fixers/envExample.js";
|
|
10
10
|
import { fixGitignore } from "./fixers/gitignore.js";
|
|
11
11
|
import { renderReport } from "./utils/report.js";
|
|
12
|
-
|
|
12
|
+
import { toSarif } from "./utils/sarif.js";
|
|
13
|
+
import { scanStaged } from "./checks/staged.js";
|
|
14
|
+
import { isGitRepo } from "./checks/history.js";
|
|
15
|
+
function printFixResults(results, showPreview = false) {
|
|
13
16
|
for (const r of results) {
|
|
17
|
+
const verb = (v) => (r.dryRun ? `would ${v.replace(/ed$/, "e")}` : v);
|
|
14
18
|
if (r.action === "created") {
|
|
15
|
-
console.log(`${pc.green("+")} created ${pc.bold(r.file)}`);
|
|
19
|
+
console.log(`${pc.green("+")} ${verb("created")} ${pc.bold(r.file)}`);
|
|
16
20
|
}
|
|
17
21
|
else if (r.action === "updated") {
|
|
18
|
-
console.log(`${pc.yellow("~")} updated ${pc.bold(r.file)}`);
|
|
22
|
+
console.log(`${pc.yellow("~")} ${verb("updated")} ${pc.bold(r.file)}`);
|
|
19
23
|
}
|
|
20
24
|
else {
|
|
21
25
|
console.log(`${pc.dim("-")} skipped ${r.file}${r.reason ? pc.dim(` (${r.reason})`) : ""}`);
|
|
22
26
|
}
|
|
27
|
+
if (showPreview && r.preview && r.action !== "skipped") {
|
|
28
|
+
const lines = r.preview.trimEnd().split("\n");
|
|
29
|
+
for (const l of lines) {
|
|
30
|
+
console.log(pc.dim(" | ") + pc.green(l));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
23
33
|
}
|
|
24
34
|
}
|
|
25
35
|
/** Resolves and validates the target directory argument. */
|
|
@@ -34,14 +44,14 @@ function resolveRoot(dir) {
|
|
|
34
44
|
return root;
|
|
35
45
|
}
|
|
36
46
|
/** Applies all safe fixes for the given root; returns the results. */
|
|
37
|
-
async function applyFixes(root, force) {
|
|
47
|
+
async function applyFixes(root, force, dryRun = false) {
|
|
38
48
|
const config = loadConfig(root);
|
|
39
49
|
const project = await detectProject(root, config);
|
|
40
50
|
const { envUsages } = scanFiles(root, project.sourceFiles, config.secretAllowlist);
|
|
41
51
|
return [
|
|
42
|
-
fixEnvExample(root, envUsages, force),
|
|
43
|
-
fixGitignore(root),
|
|
44
|
-
...fixAgentFiles(root, project, force),
|
|
52
|
+
fixEnvExample(root, envUsages, force, dryRun),
|
|
53
|
+
fixGitignore(root, dryRun),
|
|
54
|
+
...fixAgentFiles(root, project, force, dryRun),
|
|
45
55
|
];
|
|
46
56
|
}
|
|
47
57
|
/** Reads the CLI's own version from its package.json (works from dist/ at runtime). */
|
|
@@ -68,14 +78,16 @@ export function buildProgram() {
|
|
|
68
78
|
.argument("[path]", "project directory to scan (defaults to current directory)")
|
|
69
79
|
.option("-v, --verbose", "show file locations for every finding")
|
|
70
80
|
.option("--json", "output the raw report as JSON")
|
|
81
|
+
.option("--sarif", "output the report as SARIF 2.1.0 for GitHub code scanning")
|
|
71
82
|
.option("--fix", "apply safe fixes, then re-scan and show the improved report")
|
|
72
83
|
.option("--history", "also scan the full git history for leaked secrets")
|
|
73
84
|
.option("--verify", "check detected keys against provider APIs to see if they are live")
|
|
74
85
|
.action(async (dir, opts) => {
|
|
75
86
|
try {
|
|
76
87
|
const root = resolveRoot(dir);
|
|
88
|
+
const machineOutput = Boolean(opts.json || opts.sarif);
|
|
77
89
|
const scanOpts = { history: opts.history, verify: opts.verify };
|
|
78
|
-
if (opts.verify && !
|
|
90
|
+
if (opts.verify && !machineOutput) {
|
|
79
91
|
console.log(pc.dim("\n Verifying detected keys against provider APIs..."));
|
|
80
92
|
}
|
|
81
93
|
let report = await runScan(root, scanOpts);
|
|
@@ -83,7 +95,7 @@ export function buildProgram() {
|
|
|
83
95
|
const before = report.score;
|
|
84
96
|
const results = await applyFixes(root, false);
|
|
85
97
|
report = await runScan(root, scanOpts);
|
|
86
|
-
if (!
|
|
98
|
+
if (!machineOutput) {
|
|
87
99
|
console.log("");
|
|
88
100
|
console.log(pc.bold(pc.magenta("shipready check --fix")));
|
|
89
101
|
console.log("");
|
|
@@ -94,7 +106,10 @@ export function buildProgram() {
|
|
|
94
106
|
(delta > 0 ? pc.green(` (+${delta})`) : pc.dim(" (no change)")));
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
|
-
if (opts.
|
|
109
|
+
if (opts.sarif) {
|
|
110
|
+
console.log(toSarif(report, ownVersion()));
|
|
111
|
+
}
|
|
112
|
+
else if (opts.json) {
|
|
98
113
|
console.log(JSON.stringify(report, null, 2));
|
|
99
114
|
}
|
|
100
115
|
else {
|
|
@@ -140,19 +155,28 @@ export function buildProgram() {
|
|
|
140
155
|
.description("Apply safe automatic fixes (.env.example, .gitignore, agent files)")
|
|
141
156
|
.argument("[path]", "project directory (defaults to current directory)")
|
|
142
157
|
.option("-f, --force", "overwrite existing files")
|
|
158
|
+
.option("--dry-run", "show what would change without writing anything")
|
|
143
159
|
.action(async (dir, opts) => {
|
|
144
160
|
try {
|
|
145
161
|
const root = resolveRoot(dir);
|
|
162
|
+
const dryRun = opts.dryRun ?? false;
|
|
146
163
|
console.log("");
|
|
147
|
-
console.log(pc.bold(pc.magenta(
|
|
164
|
+
console.log(pc.bold(pc.magenta(`shipready fix${dryRun ? " --dry-run" : ""}`)));
|
|
148
165
|
console.log("");
|
|
149
|
-
const results = await applyFixes(root, opts.force ?? false);
|
|
150
|
-
printFixResults(results);
|
|
166
|
+
const results = await applyFixes(root, opts.force ?? false, dryRun);
|
|
167
|
+
printFixResults(results, dryRun);
|
|
151
168
|
const changed = results.filter((r) => r.action !== "skipped").length;
|
|
152
169
|
console.log("");
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
170
|
+
if (dryRun) {
|
|
171
|
+
console.log(changed > 0
|
|
172
|
+
? pc.yellow(`${changed} file${changed > 1 ? "s" : ""} would change. Run without --dry-run to apply.`)
|
|
173
|
+
: pc.dim("Nothing to fix - everything already in place."));
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
console.log(changed > 0
|
|
177
|
+
? pc.green(`${changed} file${changed > 1 ? "s" : ""} changed.`)
|
|
178
|
+
: pc.dim("Nothing to fix - everything already in place."));
|
|
179
|
+
}
|
|
156
180
|
console.log("");
|
|
157
181
|
}
|
|
158
182
|
catch (err) {
|
|
@@ -160,6 +184,54 @@ export function buildProgram() {
|
|
|
160
184
|
process.exitCode = 1;
|
|
161
185
|
}
|
|
162
186
|
});
|
|
187
|
+
program
|
|
188
|
+
.command("staged")
|
|
189
|
+
.description("Fast secrets-only scan of files staged for commit (pre-commit hook)")
|
|
190
|
+
.argument("[path]", "project directory (defaults to current directory)")
|
|
191
|
+
.action(async (dir) => {
|
|
192
|
+
try {
|
|
193
|
+
const root = resolveRoot(dir);
|
|
194
|
+
if (!isGitRepo(root)) {
|
|
195
|
+
console.error(pc.red("shipready staged: not a git repository"));
|
|
196
|
+
process.exitCode = 1;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const config = loadConfig(root);
|
|
200
|
+
const { files, secrets } = scanStaged(root, config.secretAllowlist);
|
|
201
|
+
if (files.length === 0) {
|
|
202
|
+
console.log(pc.dim("shipready staged: no staged files to scan."));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const high = secrets.filter((s) => s.confidence !== "medium");
|
|
206
|
+
const medium = secrets.filter((s) => s.confidence === "medium");
|
|
207
|
+
if (secrets.length === 0) {
|
|
208
|
+
console.log(pc.green("✓") +
|
|
209
|
+
` shipready staged: ${files.length} file${files.length > 1 ? "s" : ""} clean.`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
console.log("");
|
|
213
|
+
for (const s of high) {
|
|
214
|
+
console.log(`${pc.red("✗")} ${s.kind}: ${s.masked} ${pc.dim(`${s.file}:${s.line}`)}`);
|
|
215
|
+
}
|
|
216
|
+
for (const s of medium) {
|
|
217
|
+
console.log(`${pc.yellow("⚠")} ${s.kind}: ${s.masked} ${pc.dim(`${s.file}:${s.line}`)} ${pc.dim("(low confidence)")}`);
|
|
218
|
+
}
|
|
219
|
+
console.log("");
|
|
220
|
+
if (high.length > 0) {
|
|
221
|
+
console.log(pc.red(`Blocked: ${high.length} secret${high.length > 1 ? "s" : ""} in staged files. ` +
|
|
222
|
+
`Remove them (or add // shipready-ignore for intentional values) and retry.`));
|
|
223
|
+
// Only high-confidence findings block the commit; medium is a warning.
|
|
224
|
+
process.exitCode = 1;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
console.log(pc.yellow("Warning only - commit not blocked."));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
console.error(pc.red(`shipready staged failed: ${err.message}`));
|
|
232
|
+
process.exitCode = 1;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
163
235
|
return program;
|
|
164
236
|
}
|
|
165
237
|
/** CLI entry: parses argv and runs the matching command. */
|
|
@@ -8,7 +8,7 @@ const TARGETS = [
|
|
|
8
8
|
{ file: ".cursor/rules/shipready.md", generate: generateCursorRules },
|
|
9
9
|
];
|
|
10
10
|
/** Generates all agent instruction files, honoring --force. */
|
|
11
|
-
export function fixAgentFiles(root, project, force) {
|
|
11
|
+
export function fixAgentFiles(root, project, force, dryRun = false) {
|
|
12
12
|
const results = [];
|
|
13
13
|
for (const { file, generate } of TARGETS) {
|
|
14
14
|
if (fileExists(root, file) && !force) {
|
|
@@ -16,8 +16,11 @@ export function fixAgentFiles(root, project, force) {
|
|
|
16
16
|
continue;
|
|
17
17
|
}
|
|
18
18
|
const existed = fileExists(root, file);
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
if (!dryRun) {
|
|
20
|
+
writeTextFile(root, file, generate(project));
|
|
21
|
+
}
|
|
22
|
+
// Agent files are long; the dry-run preview stays terse on purpose.
|
|
23
|
+
results.push({ file, action: existed ? "updated" : "created", dryRun });
|
|
21
24
|
}
|
|
22
25
|
return results;
|
|
23
26
|
}
|
|
@@ -12,7 +12,7 @@ export function buildEnvExample(usages) {
|
|
|
12
12
|
return lines.join("\n");
|
|
13
13
|
}
|
|
14
14
|
/** Creates .env.example if missing (or with force). */
|
|
15
|
-
export function fixEnvExample(root, usages, force) {
|
|
15
|
+
export function fixEnvExample(root, usages, force, dryRun = false) {
|
|
16
16
|
const file = ".env.example";
|
|
17
17
|
const names = [...new Set(usages.map((u) => u.name))];
|
|
18
18
|
if (names.length === 0) {
|
|
@@ -22,6 +22,14 @@ export function fixEnvExample(root, usages, force) {
|
|
|
22
22
|
return { file, action: "skipped", reason: "already exists (use --force)" };
|
|
23
23
|
}
|
|
24
24
|
const existed = fileExists(root, file);
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const content = buildEnvExample(usages);
|
|
26
|
+
if (!dryRun) {
|
|
27
|
+
writeTextFile(root, file, content);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
file,
|
|
31
|
+
action: existed ? "updated" : "created",
|
|
32
|
+
dryRun,
|
|
33
|
+
preview: content,
|
|
34
|
+
};
|
|
27
35
|
}
|
package/dist/fixers/gitignore.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { missingIgnoreEntries } from "../checks/gitignore.js";
|
|
2
2
|
import { readTextFile, writeTextFile } from "../utils/files.js";
|
|
3
3
|
/** Adds missing important entries to .gitignore (creates it if absent). */
|
|
4
|
-
export function fixGitignore(root) {
|
|
4
|
+
export function fixGitignore(root, dryRun = false) {
|
|
5
5
|
const file = ".gitignore";
|
|
6
6
|
const existing = readTextFile(root, file);
|
|
7
7
|
if (existing === null) {
|
|
@@ -15,8 +15,10 @@ export function fixGitignore(root) {
|
|
|
15
15
|
".next",
|
|
16
16
|
"",
|
|
17
17
|
].join("\n");
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
if (!dryRun) {
|
|
19
|
+
writeTextFile(root, file, content);
|
|
20
|
+
}
|
|
21
|
+
return { file, action: "created", dryRun, preview: content };
|
|
20
22
|
}
|
|
21
23
|
const missing = missingIgnoreEntries(existing);
|
|
22
24
|
if (missing.length === 0) {
|
|
@@ -24,6 +26,9 @@ export function fixGitignore(root) {
|
|
|
24
26
|
}
|
|
25
27
|
const suffix = existing.endsWith("\n") ? "" : "\n";
|
|
26
28
|
const addition = `${suffix}\n# Added by shipready\n${missing.join("\n")}\n`;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
if (!dryRun) {
|
|
30
|
+
writeTextFile(root, file, existing + addition);
|
|
31
|
+
}
|
|
32
|
+
// Preview shows only what gets appended, not the whole file.
|
|
33
|
+
return { file, action: "updated", dryRun, preview: addition.trimStart() };
|
|
29
34
|
}
|
package/dist/scanner.js
CHANGED
|
@@ -14,6 +14,8 @@ import { isRuleDisabled, loadConfig } from "./config.js";
|
|
|
14
14
|
const CODE_ONLY = new Set([
|
|
15
15
|
".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts",
|
|
16
16
|
".vue", ".svelte", ".py", ".rb", ".go", ".rs", ".java", ".php",
|
|
17
|
+
// Inline <script> in static sites carries console.log/TODO noise too
|
|
18
|
+
".html", ".htm",
|
|
17
19
|
]);
|
|
18
20
|
/** Gathers structured project info from the given root. */
|
|
19
21
|
export async function detectProject(root, config) {
|
|
@@ -24,7 +26,7 @@ export async function detectProject(root, config) {
|
|
|
24
26
|
hasPackageJson: fileExists(root, "package.json"),
|
|
25
27
|
packageJson: pkg,
|
|
26
28
|
packageManager: detectPackageManager(root),
|
|
27
|
-
framework: detectFramework(pkg),
|
|
29
|
+
framework: detectFramework(pkg, root, sourceFiles),
|
|
28
30
|
scripts: pkg?.scripts ?? {},
|
|
29
31
|
sourceFiles,
|
|
30
32
|
};
|
package/dist/utils/files.js
CHANGED
|
@@ -3,14 +3,34 @@ import path from "node:path";
|
|
|
3
3
|
import fg from "fast-glob";
|
|
4
4
|
/** Glob patterns that are always ignored when scanning. */
|
|
5
5
|
export const IGNORE_PATTERNS = [
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
6
|
+
// Dependencies and VCS (at any depth)
|
|
7
|
+
"**/node_modules/**",
|
|
8
|
+
"**/.git/**",
|
|
9
|
+
// Build output (at any depth, e.g. mobile/build/)
|
|
10
|
+
"**/dist/**",
|
|
11
|
+
"**/build/**",
|
|
12
|
+
"**/.next/**",
|
|
13
|
+
"**/out/**",
|
|
14
|
+
"**/coverage/**",
|
|
15
|
+
"**/.cache/**",
|
|
16
|
+
"**/.turbo/**",
|
|
17
|
+
"**/.output/**",
|
|
18
|
+
// Minified/bundled vendor assets — not user code
|
|
19
|
+
"**/*.min.js",
|
|
20
|
+
"**/*.min.css",
|
|
21
|
+
"**/*.bundle.js",
|
|
22
|
+
"**/*.chunk.js",
|
|
23
|
+
"**/vendor/**",
|
|
24
|
+
"**/vendors/**",
|
|
25
|
+
// Python artifacts
|
|
26
|
+
"**/__pycache__/**",
|
|
27
|
+
"**/.venv/**",
|
|
28
|
+
"**/venv/**",
|
|
29
|
+
// Lockfiles and maps
|
|
30
|
+
"**/*.map",
|
|
31
|
+
"**/package-lock.json",
|
|
32
|
+
"**/pnpm-lock.yaml",
|
|
33
|
+
"**/yarn.lock",
|
|
14
34
|
];
|
|
15
35
|
/** Extensions we treat as scannable text/code files. */
|
|
16
36
|
export const CODE_EXTENSIONS = [
|
|
@@ -36,6 +56,9 @@ export const CODE_EXTENSIONS = [
|
|
|
36
56
|
"toml",
|
|
37
57
|
"env",
|
|
38
58
|
"sh",
|
|
59
|
+
// Static sites: inline <script> blocks can leak keys just as easily
|
|
60
|
+
"html",
|
|
61
|
+
"htm",
|
|
39
62
|
];
|
|
40
63
|
/** Returns true if the file exists (relative to root or absolute). */
|
|
41
64
|
export function fileExists(root, rel) {
|
package/dist/utils/framework.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fileExists } from "./files.js";
|
|
2
|
-
/** Detects the package manager based on lockfiles. */
|
|
2
|
+
/** Detects the package manager based on lockfiles and manifests. */
|
|
3
3
|
export function detectPackageManager(root) {
|
|
4
|
+
// Node ecosystems (lockfile wins over manifest)
|
|
4
5
|
if (fileExists(root, "pnpm-lock.yaml"))
|
|
5
6
|
return "pnpm";
|
|
6
7
|
if (fileExists(root, "yarn.lock"))
|
|
@@ -9,34 +10,131 @@ export function detectPackageManager(root) {
|
|
|
9
10
|
return "bun";
|
|
10
11
|
if (fileExists(root, "package-lock.json"))
|
|
11
12
|
return "npm";
|
|
13
|
+
if (fileExists(root, "deno.json") || fileExists(root, "deno.jsonc"))
|
|
14
|
+
return "deno";
|
|
15
|
+
// package.json without a lockfile is still an npm-family project
|
|
16
|
+
if (fileExists(root, "package.json"))
|
|
17
|
+
return "npm";
|
|
18
|
+
// Other ecosystems
|
|
19
|
+
if (fileExists(root, "uv.lock"))
|
|
20
|
+
return "uv";
|
|
21
|
+
if (fileExists(root, "poetry.lock"))
|
|
22
|
+
return "poetry";
|
|
23
|
+
if (fileExists(root, "requirements.txt") ||
|
|
24
|
+
fileExists(root, "pyproject.toml") ||
|
|
25
|
+
fileExists(root, "Pipfile")) {
|
|
26
|
+
return "pip";
|
|
27
|
+
}
|
|
28
|
+
if (fileExists(root, "Cargo.toml"))
|
|
29
|
+
return "cargo";
|
|
30
|
+
if (fileExists(root, "go.mod"))
|
|
31
|
+
return "go";
|
|
32
|
+
if (fileExists(root, "composer.json"))
|
|
33
|
+
return "composer";
|
|
34
|
+
if (fileExists(root, "Gemfile"))
|
|
35
|
+
return "bundler";
|
|
36
|
+
return "none";
|
|
37
|
+
}
|
|
38
|
+
/** Counts source files matching any of the given extensions. */
|
|
39
|
+
function countByExt(files, exts) {
|
|
40
|
+
return files.filter((f) => exts.some((e) => f.toLowerCase().endsWith(e))).length;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Detects the framework/project type. Checks package.json dependencies
|
|
44
|
+
* first, then falls back to manifest files and source file extensions so
|
|
45
|
+
* that static sites, Python, Go, Rust, PHP, and Ruby projects are
|
|
46
|
+
* identified instead of reported as "unknown".
|
|
47
|
+
*/
|
|
48
|
+
export function detectFramework(pkg, root, sourceFiles = []) {
|
|
49
|
+
if (pkg) {
|
|
50
|
+
const deps = {
|
|
51
|
+
...pkg.dependencies,
|
|
52
|
+
...pkg.devDependencies,
|
|
53
|
+
};
|
|
54
|
+
// Order matters: meta-frameworks before their underlying libraries.
|
|
55
|
+
if (deps["next"])
|
|
56
|
+
return "Next.js";
|
|
57
|
+
if (deps["astro"])
|
|
58
|
+
return "Astro";
|
|
59
|
+
if (deps["@remix-run/react"] || deps["@remix-run/node"])
|
|
60
|
+
return "Remix";
|
|
61
|
+
if (deps["@angular/core"])
|
|
62
|
+
return "Angular";
|
|
63
|
+
if (deps["gatsby"])
|
|
64
|
+
return "Gatsby";
|
|
65
|
+
if (deps["@nestjs/core"])
|
|
66
|
+
return "NestJS";
|
|
67
|
+
if (deps["nuxt"] || deps["vue"])
|
|
68
|
+
return "Vue";
|
|
69
|
+
if (deps["svelte"] || deps["@sveltejs/kit"])
|
|
70
|
+
return "Svelte";
|
|
71
|
+
if (deps["vite"])
|
|
72
|
+
return "Vite";
|
|
73
|
+
if (deps["react"])
|
|
74
|
+
return "React";
|
|
75
|
+
if (deps["express"])
|
|
76
|
+
return "Express";
|
|
77
|
+
return "Node.js";
|
|
78
|
+
}
|
|
79
|
+
// No package.json: detect by manifests, then by dominant file type.
|
|
80
|
+
if (root) {
|
|
81
|
+
if (fileExists(root, "deno.json") || fileExists(root, "deno.jsonc"))
|
|
82
|
+
return "Deno";
|
|
83
|
+
if (fileExists(root, "pyproject.toml") ||
|
|
84
|
+
fileExists(root, "requirements.txt") ||
|
|
85
|
+
fileExists(root, "Pipfile")) {
|
|
86
|
+
return "Python";
|
|
87
|
+
}
|
|
88
|
+
if (fileExists(root, "Cargo.toml"))
|
|
89
|
+
return "Rust";
|
|
90
|
+
if (fileExists(root, "go.mod"))
|
|
91
|
+
return "Go";
|
|
92
|
+
if (fileExists(root, "composer.json"))
|
|
93
|
+
return "PHP";
|
|
94
|
+
if (fileExists(root, "Gemfile"))
|
|
95
|
+
return "Ruby";
|
|
96
|
+
if (fileExists(root, "pom.xml") || fileExists(root, "build.gradle") || fileExists(root, "build.gradle.kts")) {
|
|
97
|
+
return "Java";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const html = countByExt(sourceFiles, [".html", ".htm"]);
|
|
101
|
+
const py = countByExt(sourceFiles, [".py"]);
|
|
102
|
+
const go = countByExt(sourceFiles, [".go"]);
|
|
103
|
+
const rs = countByExt(sourceFiles, [".rs"]);
|
|
104
|
+
const php = countByExt(sourceFiles, [".php"]);
|
|
105
|
+
const rb = countByExt(sourceFiles, [".rb"]);
|
|
106
|
+
const js = countByExt(sourceFiles, [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"]);
|
|
107
|
+
const counts = [
|
|
108
|
+
["Python", py],
|
|
109
|
+
["Go", go],
|
|
110
|
+
["Rust", rs],
|
|
111
|
+
["PHP", php],
|
|
112
|
+
["Ruby", rb],
|
|
113
|
+
];
|
|
114
|
+
const [lang, max] = counts.reduce((a, b) => (b[1] > a[1] ? b : a), ["unknown", 0]);
|
|
115
|
+
// A language wins only when it clearly dominates over HTML/JS glue files.
|
|
116
|
+
if (max > 0 && max >= html && max >= js)
|
|
117
|
+
return lang;
|
|
118
|
+
// HTML pages with (or without) plain JS assets: a static site.
|
|
119
|
+
if (html > 0)
|
|
120
|
+
return "Static HTML";
|
|
121
|
+
if (js > 0)
|
|
122
|
+
return "Node.js";
|
|
12
123
|
return "unknown";
|
|
13
124
|
}
|
|
14
|
-
/**
|
|
15
|
-
export function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (deps["nuxt"] || deps["vue"])
|
|
28
|
-
return "Vue";
|
|
29
|
-
if (deps["svelte"] || deps["@sveltejs/kit"])
|
|
30
|
-
return "Svelte";
|
|
31
|
-
if (deps["vite"] && deps["react"])
|
|
32
|
-
return "Vite";
|
|
33
|
-
if (deps["vite"])
|
|
34
|
-
return "Vite";
|
|
35
|
-
if (deps["react"])
|
|
36
|
-
return "React";
|
|
37
|
-
if (deps["express"])
|
|
38
|
-
return "Express";
|
|
39
|
-
return "Node.js";
|
|
125
|
+
/** True when the project belongs to the npm/Node ecosystem. */
|
|
126
|
+
export function isNodeEcosystem(framework) {
|
|
127
|
+
return ![
|
|
128
|
+
"Static HTML",
|
|
129
|
+
"Python",
|
|
130
|
+
"Go",
|
|
131
|
+
"Rust",
|
|
132
|
+
"PHP",
|
|
133
|
+
"Ruby",
|
|
134
|
+
"Java",
|
|
135
|
+
"Deno",
|
|
136
|
+
"unknown",
|
|
137
|
+
].includes(framework);
|
|
40
138
|
}
|
|
41
139
|
/** Returns the install command for a given package manager. */
|
|
42
140
|
export function installCommand(pm) {
|
|
@@ -47,8 +145,21 @@ export function installCommand(pm) {
|
|
|
47
145
|
return "yarn install";
|
|
48
146
|
case "bun":
|
|
49
147
|
return "bun install";
|
|
50
|
-
case "
|
|
51
|
-
|
|
148
|
+
case "poetry":
|
|
149
|
+
return "poetry install";
|
|
150
|
+
case "uv":
|
|
151
|
+
return "uv sync";
|
|
152
|
+
case "pip":
|
|
153
|
+
return "pip install -r requirements.txt";
|
|
154
|
+
case "cargo":
|
|
155
|
+
return "cargo build";
|
|
156
|
+
case "go":
|
|
157
|
+
return "go mod download";
|
|
158
|
+
case "composer":
|
|
159
|
+
return "composer install";
|
|
160
|
+
case "bundler":
|
|
161
|
+
return "bundle install";
|
|
162
|
+
default:
|
|
52
163
|
return "npm install";
|
|
53
164
|
}
|
|
54
165
|
}
|
|
@@ -61,8 +172,7 @@ export function runCommand(pm, script) {
|
|
|
61
172
|
return `yarn ${script}`;
|
|
62
173
|
case "bun":
|
|
63
174
|
return `bun run ${script}`;
|
|
64
|
-
|
|
65
|
-
case "unknown":
|
|
175
|
+
default:
|
|
66
176
|
return `npm run ${script}`;
|
|
67
177
|
}
|
|
68
178
|
}
|
package/dist/utils/report.js
CHANGED
|
@@ -120,11 +120,14 @@ export function renderReport(report, verbose = false, version) {
|
|
|
120
120
|
const lines = [];
|
|
121
121
|
const { project } = report;
|
|
122
122
|
const title = pc.bold(pc.magenta("shipready")) + (version ? pc.dim(` v${version}`) : "");
|
|
123
|
+
const pmLabel = project.packageManager === "none" || project.packageManager === "unknown"
|
|
124
|
+
? pc.dim("\u2014")
|
|
125
|
+
: project.packageManager;
|
|
123
126
|
const meta = pc.dim("project ") +
|
|
124
127
|
project.framework +
|
|
125
128
|
pc.dim(" \u00b7 ") +
|
|
126
129
|
pc.dim("pm ") +
|
|
127
|
-
|
|
130
|
+
pmLabel;
|
|
128
131
|
lines.push("");
|
|
129
132
|
lines.push(...box([title, meta]));
|
|
130
133
|
lines.push("");
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Maps shipready severity to a SARIF level. */
|
|
2
|
+
function toLevel(severity) {
|
|
3
|
+
if (severity === "error")
|
|
4
|
+
return "error";
|
|
5
|
+
if (severity === "warning")
|
|
6
|
+
return "warning";
|
|
7
|
+
return "note";
|
|
8
|
+
}
|
|
9
|
+
/** Human-friendly rule names derived from rule ids ("secrets.detected-item" -> "SecretsDetectedItem"). */
|
|
10
|
+
function ruleName(ruleId) {
|
|
11
|
+
return ruleId
|
|
12
|
+
.split(/[.-]/)
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.map((p) => p[0].toUpperCase() + p.slice(1))
|
|
15
|
+
.join("");
|
|
16
|
+
}
|
|
17
|
+
/** Simple stable fingerprint so GitHub tracks alerts across pushes. */
|
|
18
|
+
function fingerprint(f) {
|
|
19
|
+
const raw = `${f.rule}|${f.file ?? ""}|${f.message}`;
|
|
20
|
+
let hash = 0;
|
|
21
|
+
for (let i = 0; i < raw.length; i++) {
|
|
22
|
+
hash = (hash * 31 + raw.charCodeAt(i)) | 0;
|
|
23
|
+
}
|
|
24
|
+
return (hash >>> 0).toString(16);
|
|
25
|
+
}
|
|
26
|
+
/** Converts a shipready report to a SARIF 2.1.0 log string. */
|
|
27
|
+
export function toSarif(report, version) {
|
|
28
|
+
const rules = new Map();
|
|
29
|
+
const results = [];
|
|
30
|
+
for (const check of report.results) {
|
|
31
|
+
for (const f of check.findings) {
|
|
32
|
+
// Success/info summary rows aren't actionable alerts.
|
|
33
|
+
if (f.severity === "success")
|
|
34
|
+
continue;
|
|
35
|
+
if (f.severity === "info" && !f.file)
|
|
36
|
+
continue;
|
|
37
|
+
if (!rules.has(f.rule)) {
|
|
38
|
+
rules.set(f.rule, {
|
|
39
|
+
id: f.rule,
|
|
40
|
+
name: ruleName(f.rule),
|
|
41
|
+
shortDescription: { text: f.rule },
|
|
42
|
+
defaultConfiguration: { level: toLevel(f.severity) },
|
|
43
|
+
helpUri: "https://github.com/formalness/shipready#readme",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
results.push({
|
|
47
|
+
ruleId: f.rule,
|
|
48
|
+
level: toLevel(f.severity),
|
|
49
|
+
message: { text: f.message },
|
|
50
|
+
locations: [
|
|
51
|
+
{
|
|
52
|
+
physicalLocation: {
|
|
53
|
+
artifactLocation: {
|
|
54
|
+
// SARIF requires a location; findings without a file anchor
|
|
55
|
+
// to the repo root via package.json-ish convention.
|
|
56
|
+
uri: f.file ?? ".",
|
|
57
|
+
uriBaseId: "%SRCROOT%",
|
|
58
|
+
},
|
|
59
|
+
...(f.line && f.line > 0 ? { region: { startLine: f.line } } : {}),
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
partialFingerprints: { shipready: fingerprint(f) },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const log = {
|
|
68
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
69
|
+
version: "2.1.0",
|
|
70
|
+
runs: [
|
|
71
|
+
{
|
|
72
|
+
tool: {
|
|
73
|
+
driver: {
|
|
74
|
+
name: "shipready",
|
|
75
|
+
version,
|
|
76
|
+
informationUri: "https://github.com/formalness/shipready",
|
|
77
|
+
rules: [...rules.values()],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
results,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
return JSON.stringify(log, null, 2);
|
|
85
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shipready",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Pre-flight check for your repo before shipping. Scans AI-coded projects for secrets, env issues, debug leftovers, and generates AI-agent instruction files.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|