@adhix11/shipguard 0.1.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/LICENSE +21 -0
- package/README.md +183 -0
- package/dist/index.js +733 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 adhix11
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# ๐ข ShipGuard
|
|
2
|
+
|
|
3
|
+
**Catch stupid production mistakes before you ship.**
|
|
4
|
+
|
|
5
|
+
A zero-config preflight scanner for JavaScript and TypeScript projects. Detect secrets, missing env docs, debug code, risky test flags, and basic package readiness before you ship.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@adhix11/shipguard)
|
|
8
|
+
[](https://github.com/adhix11/shipguard/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why?
|
|
13
|
+
|
|
14
|
+
Developers often forget small but dangerous things before pushing code:
|
|
15
|
+
|
|
16
|
+
| Pain | Example |
|
|
17
|
+
|------|---------|
|
|
18
|
+
| **Secret leakage** | AWS key, JWT secret, DB password inside code |
|
|
19
|
+
| **Missing env docs** | Code uses `process.env.DB_URL`, but `.env.example` doesn't mention it |
|
|
20
|
+
| **Debug code** | `console.log`, `debugger`, temporary test code left in |
|
|
21
|
+
| **Test mistakes** | `describe.only`, `it.only`, skipped tests |
|
|
22
|
+
| **Risky package scripts** | Suspicious `postinstall`, `preinstall`, shell commands |
|
|
23
|
+
| **Poor release readiness** | No README, no LICENSE, missing package.json fields |
|
|
24
|
+
|
|
25
|
+
With AI-generated code accelerating development, the bottleneck has shifted from **writing code** to **reviewing and validating it**. ShipGuard helps you catch the things that slip through.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
Run with zero setup:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx @adhix11/shipguard
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or install globally:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g @adhix11/shipguard
|
|
41
|
+
shipguard
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## What It Scans
|
|
47
|
+
|
|
48
|
+
### ๐ Secrets Risk
|
|
49
|
+
- AWS Access Keys & Secret Keys
|
|
50
|
+
- Private key blocks (`-----BEGIN PRIVATE KEY-----`)
|
|
51
|
+
- MongoDB connection URIs (`mongodb+srv://...`)
|
|
52
|
+
- Hardcoded passwords (`password = "..."`)
|
|
53
|
+
- JWT secrets
|
|
54
|
+
- API keys & tokens (GitHub, OpenAI, Stripe, Slack)
|
|
55
|
+
|
|
56
|
+
### ๐ Missing Env Documentation
|
|
57
|
+
- Finds all `process.env.XYZ` and `import.meta.env.XYZ` in your code
|
|
58
|
+
- Cross-references with `.env.example`
|
|
59
|
+
- Reports any environment variables missing from documentation
|
|
60
|
+
|
|
61
|
+
### ๐ Debug Code
|
|
62
|
+
- `console.log`, `console.debug`, `console.warn`
|
|
63
|
+
- `debugger` statements
|
|
64
|
+
- `TODO`, `FIXME`, `HACK`, `XXX` comments
|
|
65
|
+
- `alert()` calls
|
|
66
|
+
|
|
67
|
+
### ๐งช Test Risk
|
|
68
|
+
- `describe.only()`, `it.only()`, `test.only()` โ focused tests
|
|
69
|
+
- `describe.skip()`, `it.skip()`, `test.skip()` โ skipped tests
|
|
70
|
+
- `fdescribe`, `fit`, `xit`, `xdescribe` โ Jasmine equivalents
|
|
71
|
+
|
|
72
|
+
### ๐ฆ Package Health
|
|
73
|
+
- README.md exists
|
|
74
|
+
- LICENSE exists
|
|
75
|
+
- `package.json` has `name`, `version`, `description`
|
|
76
|
+
- Entry point (`main`, `bin`, `module`, or `exports`) is defined
|
|
77
|
+
- Risky lifecycle scripts (`postinstall`, `preinstall`) with suspicious commands
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Example Output
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
๐ข ShipGuard Report
|
|
85
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
86
|
+
|
|
87
|
+
โ Secrets Risk
|
|
88
|
+
src/config/db.ts
|
|
89
|
+
โ Possible MongoDB URI found (line 8)
|
|
90
|
+
|
|
91
|
+
โ ๏ธ Missing Env Documentation
|
|
92
|
+
.env.example
|
|
93
|
+
โ .env.example is missing 3 variables:
|
|
94
|
+
โ Missing: DB_URL
|
|
95
|
+
โ Missing: JWT_SECRET
|
|
96
|
+
โ Missing: AWS_REGION
|
|
97
|
+
|
|
98
|
+
โ ๏ธ Debug Code Found
|
|
99
|
+
src/app.ts
|
|
100
|
+
โ console.log found (line 42)
|
|
101
|
+
|
|
102
|
+
โ Test Risk
|
|
103
|
+
src/__tests__/user.test.ts
|
|
104
|
+
โ it.only found โ other tests will be skipped (line 18)
|
|
105
|
+
|
|
106
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
107
|
+
Summary:
|
|
108
|
+
2 critical issues
|
|
109
|
+
4 warnings
|
|
110
|
+
|
|
111
|
+
โ Run failed. Fix critical issues before shipping.
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## CLI Options
|
|
117
|
+
|
|
118
|
+
| Option | Description |
|
|
119
|
+
|--------|-------------|
|
|
120
|
+
| `--strict` | Treat warnings as errors (exit code 1 for any issue) |
|
|
121
|
+
| `--json` | Output results as JSON for CI/CD integration |
|
|
122
|
+
| `--ignore <dirs>` | Comma-separated directories to ignore |
|
|
123
|
+
| `--help`, `-h` | Show help message |
|
|
124
|
+
| `--version`, `-v` | Show version number |
|
|
125
|
+
|
|
126
|
+
### Examples
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Basic scan
|
|
130
|
+
npx @adhix11/shipguard
|
|
131
|
+
|
|
132
|
+
# Strict mode โ fail on any warning
|
|
133
|
+
npx @adhix11/shipguard --strict
|
|
134
|
+
|
|
135
|
+
# JSON output for CI pipelines
|
|
136
|
+
npx @adhix11/shipguard --json
|
|
137
|
+
|
|
138
|
+
# Ignore specific directories
|
|
139
|
+
npx @adhix11/shipguard --ignore "dist,build,coverage"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Default Ignores
|
|
145
|
+
|
|
146
|
+
ShipGuard automatically skips these directories:
|
|
147
|
+
|
|
148
|
+
`node_modules`, `dist`, `build`, `.git`, `.next`, `.nuxt`, `coverage`, `.cache`, `.turbo`, `.output`, `out`
|
|
149
|
+
|
|
150
|
+
And these files: `*.min.js`, `*.min.css`, `*.map`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## CI/CD Integration
|
|
155
|
+
|
|
156
|
+
### GitHub Actions
|
|
157
|
+
|
|
158
|
+
```yaml
|
|
159
|
+
- name: ShipGuard Preflight Check
|
|
160
|
+
run: npx @adhix11/shipguard --strict
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Pre-commit Hook (with Husky)
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npx husky add .husky/pre-commit "npx @adhix11/shipguard"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Exit Codes
|
|
172
|
+
|
|
173
|
+
| Code | Meaning |
|
|
174
|
+
|------|---------|
|
|
175
|
+
| `0` | All clear (or warnings only without `--strict`) |
|
|
176
|
+
| `1` | Critical issues found (or warnings in `--strict` mode) |
|
|
177
|
+
| `2` | ShipGuard internal error |
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT ยฉ [adhix11](https://github.com/adhix11)
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { resolve as resolve4 } from "path";
|
|
5
|
+
import pc2 from "picocolors";
|
|
6
|
+
|
|
7
|
+
// src/utils/readFiles.ts
|
|
8
|
+
import fg from "fast-glob";
|
|
9
|
+
import { readFile } from "fs/promises";
|
|
10
|
+
import { resolve } from "path";
|
|
11
|
+
var DEFAULT_IGNORES = [
|
|
12
|
+
"node_modules",
|
|
13
|
+
"dist",
|
|
14
|
+
"build",
|
|
15
|
+
".git",
|
|
16
|
+
".next",
|
|
17
|
+
".nuxt",
|
|
18
|
+
"coverage",
|
|
19
|
+
".cache",
|
|
20
|
+
".turbo",
|
|
21
|
+
".output",
|
|
22
|
+
"out",
|
|
23
|
+
"*.min.js",
|
|
24
|
+
"*.min.css",
|
|
25
|
+
"*.map",
|
|
26
|
+
"package-lock.json",
|
|
27
|
+
"yarn.lock",
|
|
28
|
+
"pnpm-lock.yaml"
|
|
29
|
+
];
|
|
30
|
+
var SCAN_EXTENSIONS = [
|
|
31
|
+
"ts",
|
|
32
|
+
"tsx",
|
|
33
|
+
"js",
|
|
34
|
+
"jsx",
|
|
35
|
+
"mjs",
|
|
36
|
+
"cjs",
|
|
37
|
+
"json",
|
|
38
|
+
"yaml",
|
|
39
|
+
"yml",
|
|
40
|
+
"env",
|
|
41
|
+
"env.local",
|
|
42
|
+
"env.development",
|
|
43
|
+
"env.production",
|
|
44
|
+
"env.test",
|
|
45
|
+
"toml",
|
|
46
|
+
"cfg",
|
|
47
|
+
"conf",
|
|
48
|
+
"ini"
|
|
49
|
+
];
|
|
50
|
+
async function getFiles(cwd, extraIgnores = []) {
|
|
51
|
+
const ignorePatterns = [...DEFAULT_IGNORES, ...extraIgnores].map(
|
|
52
|
+
(pattern) => `**/${pattern}/**`
|
|
53
|
+
);
|
|
54
|
+
const extensionGlob = `**/*.{${SCAN_EXTENSIONS.join(",")}}`;
|
|
55
|
+
const paths = await fg(extensionGlob, {
|
|
56
|
+
cwd,
|
|
57
|
+
ignore: ignorePatterns,
|
|
58
|
+
dot: true,
|
|
59
|
+
absolute: false,
|
|
60
|
+
onlyFiles: true
|
|
61
|
+
});
|
|
62
|
+
const files = [];
|
|
63
|
+
for (const relativePath of paths) {
|
|
64
|
+
try {
|
|
65
|
+
const absolutePath = resolve(cwd, relativePath);
|
|
66
|
+
const content = await readFile(absolutePath, "utf-8");
|
|
67
|
+
const lines = content.split("\n");
|
|
68
|
+
files.push({ relativePath, absolutePath, content, lines });
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
74
|
+
async function readFileContent(filePath) {
|
|
75
|
+
try {
|
|
76
|
+
return await readFile(filePath, "utf-8");
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/utils/report.ts
|
|
83
|
+
import pc from "picocolors";
|
|
84
|
+
function printReport(results, options) {
|
|
85
|
+
if (options.json) {
|
|
86
|
+
printJsonReport(results);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
printFormattedReport(results, options);
|
|
90
|
+
}
|
|
91
|
+
function getExitCode(results, options) {
|
|
92
|
+
const criticalCount = countBySeverity(results, "critical");
|
|
93
|
+
const warningCount = countBySeverity(results, "warning");
|
|
94
|
+
if (criticalCount > 0) return 1;
|
|
95
|
+
if (options.strict && warningCount > 0) return 1;
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
function printJsonReport(results) {
|
|
99
|
+
const output = {
|
|
100
|
+
tool: "@adhix11/shipguard",
|
|
101
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
102
|
+
results: results.map((r) => ({
|
|
103
|
+
scanner: r.name,
|
|
104
|
+
issues: r.issues.map((i) => ({
|
|
105
|
+
file: i.file,
|
|
106
|
+
line: i.line ?? null,
|
|
107
|
+
message: i.message,
|
|
108
|
+
severity: i.severity
|
|
109
|
+
}))
|
|
110
|
+
})),
|
|
111
|
+
summary: {
|
|
112
|
+
critical: countBySeverity(results, "critical"),
|
|
113
|
+
warnings: countBySeverity(results, "warning"),
|
|
114
|
+
info: countBySeverity(results, "info"),
|
|
115
|
+
totalIssues: results.reduce((sum, r) => sum + r.issues.length, 0)
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
console.log(JSON.stringify(output, null, 2));
|
|
119
|
+
}
|
|
120
|
+
function printFormattedReport(results, options) {
|
|
121
|
+
console.log();
|
|
122
|
+
console.log(
|
|
123
|
+
pc.bold(pc.cyan("\u{1F6A2} ShipGuard Report"))
|
|
124
|
+
);
|
|
125
|
+
console.log(pc.dim("\u2500".repeat(50)));
|
|
126
|
+
console.log();
|
|
127
|
+
let hasIssues = false;
|
|
128
|
+
for (const result of results) {
|
|
129
|
+
if (result.issues.length === 0) continue;
|
|
130
|
+
hasIssues = true;
|
|
131
|
+
const hasCritical = result.issues.some((i) => i.severity === "critical");
|
|
132
|
+
const headerColor = hasCritical ? pc.red : pc.yellow;
|
|
133
|
+
const icon = hasCritical ? "\u274C" : "\u26A0\uFE0F";
|
|
134
|
+
console.log(headerColor(pc.bold(`${icon} ${result.name}`)));
|
|
135
|
+
const byFile = groupByFile(result);
|
|
136
|
+
for (const [file, issues] of Object.entries(byFile)) {
|
|
137
|
+
console.log(pc.dim(` ${file}`));
|
|
138
|
+
for (const issue of issues) {
|
|
139
|
+
const lineInfo = issue.line ? pc.dim(` (line ${issue.line})`) : "";
|
|
140
|
+
const bullet = issue.severity === "critical" ? pc.red(" \u2717") : issue.severity === "warning" ? pc.yellow(" \u25CF") : pc.blue(" \u25CB");
|
|
141
|
+
console.log(`${bullet} ${issue.message}${lineInfo}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
console.log();
|
|
145
|
+
}
|
|
146
|
+
const criticalCount = countBySeverity(results, "critical");
|
|
147
|
+
const warningCount = countBySeverity(results, "warning");
|
|
148
|
+
const infoCount = countBySeverity(results, "info");
|
|
149
|
+
console.log(pc.dim("\u2500".repeat(50)));
|
|
150
|
+
console.log(pc.bold("Summary:"));
|
|
151
|
+
if (criticalCount > 0) {
|
|
152
|
+
console.log(pc.red(` ${criticalCount} critical issue${criticalCount !== 1 ? "s" : ""}`));
|
|
153
|
+
}
|
|
154
|
+
if (warningCount > 0) {
|
|
155
|
+
console.log(pc.yellow(` ${warningCount} warning${warningCount !== 1 ? "s" : ""}`));
|
|
156
|
+
}
|
|
157
|
+
if (infoCount > 0) {
|
|
158
|
+
console.log(pc.blue(` ${infoCount} info`));
|
|
159
|
+
}
|
|
160
|
+
console.log();
|
|
161
|
+
const exitCode = getExitCode(results, options);
|
|
162
|
+
if (!hasIssues) {
|
|
163
|
+
console.log(pc.green(pc.bold("\u2714 All clear! Ship with confidence. \u{1F680}")));
|
|
164
|
+
} else if (exitCode === 0) {
|
|
165
|
+
console.log(
|
|
166
|
+
pc.yellow(pc.bold("\u26A0 Warnings found. Review before shipping."))
|
|
167
|
+
);
|
|
168
|
+
} else {
|
|
169
|
+
console.log(
|
|
170
|
+
pc.red(pc.bold("\u2717 Run failed. Fix critical issues before shipping."))
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
console.log();
|
|
174
|
+
}
|
|
175
|
+
function countBySeverity(results, severity) {
|
|
176
|
+
return results.reduce(
|
|
177
|
+
(sum, r) => sum + r.issues.filter((i) => i.severity === severity).length,
|
|
178
|
+
0
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
function groupByFile(result) {
|
|
182
|
+
const grouped = {};
|
|
183
|
+
for (const issue of result.issues) {
|
|
184
|
+
const key = issue.file || "(project)";
|
|
185
|
+
if (!grouped[key]) grouped[key] = [];
|
|
186
|
+
grouped[key].push(issue);
|
|
187
|
+
}
|
|
188
|
+
return grouped;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/scanners/secrets.ts
|
|
192
|
+
var SECRET_PATTERNS = [
|
|
193
|
+
{
|
|
194
|
+
pattern: /AWS_ACCESS_KEY_ID\s*[=:]\s*['"]?[A-Z0-9]{16,}/i,
|
|
195
|
+
label: "Possible AWS Access Key found"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
pattern: /AWS_SECRET_ACCESS_KEY\s*[=:]\s*['"]?[A-Za-z0-9/+=]{30,}/i,
|
|
199
|
+
label: "Possible AWS Secret Key found"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
pattern: /AKIA[0-9A-Z]{16}/,
|
|
203
|
+
label: "AWS Access Key ID pattern detected"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
pattern: /SECRET_KEY\s*[=:]\s*['"]?[^\s'"]{8,}/i,
|
|
207
|
+
label: "Possible SECRET_KEY assignment found"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
pattern: /PRIVATE_KEY\s*[=:]\s*['"]?[^\s'"]{8,}/i,
|
|
211
|
+
label: "Possible PRIVATE_KEY assignment found"
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/,
|
|
215
|
+
label: "Private key block detected"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
pattern: /mongodb\+srv:\/\/[^\s'"]+/i,
|
|
219
|
+
label: "Possible MongoDB connection URI found"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
pattern: /mongodb:\/\/[^\s'"]+/i,
|
|
223
|
+
label: "Possible MongoDB connection URI found"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
pattern: /password\s*[=:]\s*['"][^'"]{4,}['"]/i,
|
|
227
|
+
label: "Possible hardcoded password found"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
pattern: /jwtSecret\s*[=:]\s*['"][^'"]+['"]/i,
|
|
231
|
+
label: "Possible JWT secret found"
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
pattern: /jwt_secret\s*[=:]\s*['"][^'"]+['"]/i,
|
|
235
|
+
label: "Possible JWT secret found"
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
pattern: /api[_-]?key\s*[=:]\s*['"][^'"]{8,}['"]/i,
|
|
239
|
+
label: "Possible API key found"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
pattern: /api[_-]?secret\s*[=:]\s*['"][^'"]{8,}['"]/i,
|
|
243
|
+
label: "Possible API secret found"
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
pattern: /ghp_[A-Za-z0-9_]{36,}/,
|
|
247
|
+
label: "GitHub personal access token detected"
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
pattern: /sk-[A-Za-z0-9]{32,}/,
|
|
251
|
+
label: "Possible OpenAI/Stripe secret key detected"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
pattern: /xox[bpras]-[A-Za-z0-9-]+/,
|
|
255
|
+
label: "Possible Slack token detected"
|
|
256
|
+
}
|
|
257
|
+
];
|
|
258
|
+
var SKIP_PATTERNS = [
|
|
259
|
+
/\.env\.example$/,
|
|
260
|
+
/\.env\.sample$/,
|
|
261
|
+
/\.env\.template$/,
|
|
262
|
+
/package-lock\.json$/,
|
|
263
|
+
/yarn\.lock$/,
|
|
264
|
+
/pnpm-lock\.yaml$/
|
|
265
|
+
];
|
|
266
|
+
function scanSecrets(files) {
|
|
267
|
+
const issues = [];
|
|
268
|
+
for (const file of files) {
|
|
269
|
+
if (SKIP_PATTERNS.some((p) => p.test(file.relativePath))) continue;
|
|
270
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
271
|
+
const line = file.lines[i];
|
|
272
|
+
for (const { pattern, label } of SECRET_PATTERNS) {
|
|
273
|
+
if (pattern.test(line)) {
|
|
274
|
+
issues.push({
|
|
275
|
+
file: file.relativePath,
|
|
276
|
+
line: i + 1,
|
|
277
|
+
message: label,
|
|
278
|
+
severity: "critical"
|
|
279
|
+
});
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
name: "Secrets Risk",
|
|
287
|
+
icon: "\u{1F510}",
|
|
288
|
+
issues
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/scanners/env.ts
|
|
293
|
+
import { resolve as resolve2 } from "path";
|
|
294
|
+
var ENV_USAGE_PATTERN = /process\.env\.([A-Z_][A-Z0-9_]*)/g;
|
|
295
|
+
var VITE_ENV_PATTERN = /import\.meta\.env\.([A-Z_][A-Z0-9_]*)/g;
|
|
296
|
+
var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
|
|
297
|
+
"NODE_ENV",
|
|
298
|
+
"PORT",
|
|
299
|
+
"HOME",
|
|
300
|
+
"PATH",
|
|
301
|
+
"USER",
|
|
302
|
+
"SHELL",
|
|
303
|
+
"TERM",
|
|
304
|
+
"LANG",
|
|
305
|
+
"PWD",
|
|
306
|
+
"HOSTNAME",
|
|
307
|
+
"CI",
|
|
308
|
+
"TZ",
|
|
309
|
+
"npm_package_version",
|
|
310
|
+
"npm_package_name"
|
|
311
|
+
]);
|
|
312
|
+
async function scanEnv(files, cwd) {
|
|
313
|
+
const issues = [];
|
|
314
|
+
const usedEnvVars = /* @__PURE__ */ new Map();
|
|
315
|
+
for (const file of files) {
|
|
316
|
+
if (/\.env/.test(file.relativePath)) continue;
|
|
317
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
318
|
+
const line = file.lines[i];
|
|
319
|
+
for (const pattern of [ENV_USAGE_PATTERN, VITE_ENV_PATTERN]) {
|
|
320
|
+
pattern.lastIndex = 0;
|
|
321
|
+
let match;
|
|
322
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
323
|
+
const varName = match[1];
|
|
324
|
+
if (BUILTIN_ENV_VARS.has(varName)) continue;
|
|
325
|
+
if (!usedEnvVars.has(varName)) {
|
|
326
|
+
usedEnvVars.set(varName, []);
|
|
327
|
+
}
|
|
328
|
+
usedEnvVars.get(varName).push({
|
|
329
|
+
file: file.relativePath,
|
|
330
|
+
line: i + 1
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (usedEnvVars.size === 0) {
|
|
337
|
+
return { name: "Missing Env Documentation", icon: "\u{1F4CB}", issues };
|
|
338
|
+
}
|
|
339
|
+
const envExamplePath = resolve2(cwd, ".env.example");
|
|
340
|
+
const envExampleContent = await readFileContent(envExamplePath);
|
|
341
|
+
if (envExampleContent === null) {
|
|
342
|
+
issues.push({
|
|
343
|
+
file: ".env.example",
|
|
344
|
+
message: `.env.example file is missing. ${usedEnvVars.size} env variable${usedEnvVars.size !== 1 ? "s" : ""} found in code.`,
|
|
345
|
+
severity: "warning"
|
|
346
|
+
});
|
|
347
|
+
for (const [varName] of usedEnvVars) {
|
|
348
|
+
issues.push({
|
|
349
|
+
file: ".env.example",
|
|
350
|
+
message: `Missing: ${varName}`,
|
|
351
|
+
severity: "warning"
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
const documentedVars = /* @__PURE__ */ new Set();
|
|
356
|
+
for (const line of envExampleContent.split("\n")) {
|
|
357
|
+
const trimmed = line.trim();
|
|
358
|
+
if (trimmed.startsWith("#") || trimmed === "") continue;
|
|
359
|
+
const eqIndex = trimmed.indexOf("=");
|
|
360
|
+
const varName = eqIndex !== -1 ? trimmed.slice(0, eqIndex).trim() : trimmed.trim();
|
|
361
|
+
if (varName) documentedVars.add(varName);
|
|
362
|
+
}
|
|
363
|
+
const missingVars = [];
|
|
364
|
+
for (const [varName] of usedEnvVars) {
|
|
365
|
+
if (!documentedVars.has(varName)) {
|
|
366
|
+
missingVars.push(varName);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (missingVars.length > 0) {
|
|
370
|
+
issues.push({
|
|
371
|
+
file: ".env.example",
|
|
372
|
+
message: `.env.example is missing ${missingVars.length} variable${missingVars.length !== 1 ? "s" : ""}:`,
|
|
373
|
+
severity: "warning"
|
|
374
|
+
});
|
|
375
|
+
for (const varName of missingVars) {
|
|
376
|
+
issues.push({
|
|
377
|
+
file: ".env.example",
|
|
378
|
+
message: `Missing: ${varName}`,
|
|
379
|
+
severity: "warning"
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return { name: "Missing Env Documentation", icon: "\u{1F4CB}", issues };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/scanners/debug.ts
|
|
388
|
+
var DEBUG_PATTERNS = [
|
|
389
|
+
{
|
|
390
|
+
pattern: /\bconsole\.log\s*\(/,
|
|
391
|
+
label: "console.log found"
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
pattern: /\bconsole\.debug\s*\(/,
|
|
395
|
+
label: "console.debug found"
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
pattern: /\bconsole\.warn\s*\(/,
|
|
399
|
+
label: "console.warn found"
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
pattern: /\bdebugger\b/,
|
|
403
|
+
label: "debugger statement found"
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
pattern: /\/\/\s*TODO\b/i,
|
|
407
|
+
label: "TODO comment found"
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
pattern: /\/\/\s*FIXME\b/i,
|
|
411
|
+
label: "FIXME comment found"
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
pattern: /\/\/\s*HACK\b/i,
|
|
415
|
+
label: "HACK comment found"
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
pattern: /\/\/\s*XXX\b/i,
|
|
419
|
+
label: "XXX marker found"
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
pattern: /\balert\s*\(/,
|
|
423
|
+
label: "alert() call found"
|
|
424
|
+
}
|
|
425
|
+
];
|
|
426
|
+
var SKIP_PATTERNS2 = [
|
|
427
|
+
/\.config\.(ts|js|mjs|cjs)$/,
|
|
428
|
+
/tsup\.config/,
|
|
429
|
+
/vite\.config/,
|
|
430
|
+
/next\.config/,
|
|
431
|
+
/webpack\.config/,
|
|
432
|
+
/jest\.config/,
|
|
433
|
+
/eslint/,
|
|
434
|
+
/prettier/
|
|
435
|
+
];
|
|
436
|
+
function scanDebug(files) {
|
|
437
|
+
const issues = [];
|
|
438
|
+
for (const file of files) {
|
|
439
|
+
if (SKIP_PATTERNS2.some((p) => p.test(file.relativePath))) continue;
|
|
440
|
+
if (!/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(file.relativePath)) continue;
|
|
441
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
442
|
+
const line = file.lines[i];
|
|
443
|
+
for (const { pattern, label } of DEBUG_PATTERNS) {
|
|
444
|
+
if (pattern.test(line)) {
|
|
445
|
+
issues.push({
|
|
446
|
+
file: file.relativePath,
|
|
447
|
+
line: i + 1,
|
|
448
|
+
message: label,
|
|
449
|
+
severity: "warning"
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
name: "Debug Code Found",
|
|
457
|
+
icon: "\u{1F41B}",
|
|
458
|
+
issues
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/scanners/tests.ts
|
|
463
|
+
var TEST_RISK_PATTERNS = [
|
|
464
|
+
{
|
|
465
|
+
pattern: /\bdescribe\.only\s*\(/,
|
|
466
|
+
label: "describe.only found \u2014 other tests will be skipped"
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
pattern: /\bit\.only\s*\(/,
|
|
470
|
+
label: "it.only found \u2014 other tests will be skipped"
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
pattern: /\btest\.only\s*\(/,
|
|
474
|
+
label: "test.only found \u2014 other tests will be skipped"
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
pattern: /\bdescribe\.skip\s*\(/,
|
|
478
|
+
label: "describe.skip found \u2014 tests are being skipped"
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
pattern: /\bit\.skip\s*\(/,
|
|
482
|
+
label: "it.skip found \u2014 test is being skipped"
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
pattern: /\btest\.skip\s*\(/,
|
|
486
|
+
label: "test.skip found \u2014 test is being skipped"
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
pattern: /\bfdescribe\s*\(/,
|
|
490
|
+
label: "fdescribe found \u2014 focused test suite (Jasmine)"
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
pattern: /\bfit\s*\(/,
|
|
494
|
+
label: "fit found \u2014 focused test (Jasmine)"
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
pattern: /\bxit\s*\(/,
|
|
498
|
+
label: "xit found \u2014 excluded test (Jasmine)"
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
pattern: /\bxdescribe\s*\(/,
|
|
502
|
+
label: "xdescribe found \u2014 excluded test suite (Jasmine)"
|
|
503
|
+
}
|
|
504
|
+
];
|
|
505
|
+
var TEST_FILE_PATTERN = /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
506
|
+
function scanTests(files) {
|
|
507
|
+
const issues = [];
|
|
508
|
+
const testFiles = files.filter((f) => TEST_FILE_PATTERN.test(f.relativePath));
|
|
509
|
+
for (const file of testFiles) {
|
|
510
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
511
|
+
const line = file.lines[i];
|
|
512
|
+
for (const { pattern, label } of TEST_RISK_PATTERNS) {
|
|
513
|
+
if (pattern.test(line)) {
|
|
514
|
+
issues.push({
|
|
515
|
+
file: file.relativePath,
|
|
516
|
+
line: i + 1,
|
|
517
|
+
message: label,
|
|
518
|
+
severity: "critical"
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
name: "Test Risk",
|
|
526
|
+
icon: "\u{1F9EA}",
|
|
527
|
+
issues
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/scanners/packageHealth.ts
|
|
532
|
+
import { resolve as resolve3 } from "path";
|
|
533
|
+
import { access, constants } from "fs/promises";
|
|
534
|
+
var RISKY_SCRIPTS = ["postinstall", "preinstall", "install"];
|
|
535
|
+
var SUSPICIOUS_PATTERNS = [
|
|
536
|
+
/curl\s/,
|
|
537
|
+
/wget\s/,
|
|
538
|
+
/sh\s+-c/,
|
|
539
|
+
/bash\s+-c/,
|
|
540
|
+
/powershell/i,
|
|
541
|
+
/eval\s/,
|
|
542
|
+
/\brm\s+-rf/,
|
|
543
|
+
/>&\s*\/dev\/null/,
|
|
544
|
+
/\.sh\b/
|
|
545
|
+
];
|
|
546
|
+
async function scanPackageHealth(cwd) {
|
|
547
|
+
const issues = [];
|
|
548
|
+
if (!await fileExists(resolve3(cwd, "README.md"))) {
|
|
549
|
+
issues.push({
|
|
550
|
+
file: "README.md",
|
|
551
|
+
message: "README.md is missing",
|
|
552
|
+
severity: "warning"
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
if (!await fileExists(resolve3(cwd, "LICENSE"))) {
|
|
556
|
+
issues.push({
|
|
557
|
+
file: "LICENSE",
|
|
558
|
+
message: "LICENSE file is missing",
|
|
559
|
+
severity: "warning"
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
const pkgContent = await readFileContent(resolve3(cwd, "package.json"));
|
|
563
|
+
if (!pkgContent) {
|
|
564
|
+
issues.push({
|
|
565
|
+
file: "package.json",
|
|
566
|
+
message: "package.json not found",
|
|
567
|
+
severity: "critical"
|
|
568
|
+
});
|
|
569
|
+
return { name: "Package Health", icon: "\u{1F4E6}", issues };
|
|
570
|
+
}
|
|
571
|
+
let pkg;
|
|
572
|
+
try {
|
|
573
|
+
pkg = JSON.parse(pkgContent);
|
|
574
|
+
} catch {
|
|
575
|
+
issues.push({
|
|
576
|
+
file: "package.json",
|
|
577
|
+
message: "package.json contains invalid JSON",
|
|
578
|
+
severity: "critical"
|
|
579
|
+
});
|
|
580
|
+
return { name: "Package Health", icon: "\u{1F4E6}", issues };
|
|
581
|
+
}
|
|
582
|
+
const requiredFields = ["name", "version", "description"];
|
|
583
|
+
for (const field of requiredFields) {
|
|
584
|
+
if (!pkg[field]) {
|
|
585
|
+
issues.push({
|
|
586
|
+
file: "package.json",
|
|
587
|
+
message: `Missing required field: "${field}"`,
|
|
588
|
+
severity: "warning"
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (!pkg["main"] && !pkg["bin"] && !pkg["exports"] && !pkg["module"]) {
|
|
593
|
+
issues.push({
|
|
594
|
+
file: "package.json",
|
|
595
|
+
message: 'Missing entry point: needs "main", "bin", "module", or "exports"',
|
|
596
|
+
severity: "warning"
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
const scripts = pkg["scripts"];
|
|
600
|
+
if (scripts) {
|
|
601
|
+
for (const scriptName of RISKY_SCRIPTS) {
|
|
602
|
+
const scriptValue = scripts[scriptName];
|
|
603
|
+
if (!scriptValue) continue;
|
|
604
|
+
const isSuspicious = SUSPICIOUS_PATTERNS.some(
|
|
605
|
+
(p) => p.test(scriptValue)
|
|
606
|
+
);
|
|
607
|
+
if (isSuspicious) {
|
|
608
|
+
issues.push({
|
|
609
|
+
file: "package.json",
|
|
610
|
+
message: `Risky lifecycle script "${scriptName}": ${scriptValue}`,
|
|
611
|
+
severity: "critical"
|
|
612
|
+
});
|
|
613
|
+
} else {
|
|
614
|
+
issues.push({
|
|
615
|
+
file: "package.json",
|
|
616
|
+
message: `Lifecycle script "${scriptName}" found: ${scriptValue}`,
|
|
617
|
+
severity: "info"
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return { name: "Package Health", icon: "\u{1F4E6}", issues };
|
|
623
|
+
}
|
|
624
|
+
async function fileExists(filePath) {
|
|
625
|
+
try {
|
|
626
|
+
await access(filePath, constants.F_OK);
|
|
627
|
+
return true;
|
|
628
|
+
} catch {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/index.ts
|
|
634
|
+
var VERSION = "0.1.0";
|
|
635
|
+
var HELP_TEXT = `
|
|
636
|
+
${pc2.bold(pc2.cyan("\u{1F6A2} ShipGuard"))} v${VERSION}
|
|
637
|
+
${pc2.dim("Catch stupid production mistakes before you ship.")}
|
|
638
|
+
|
|
639
|
+
${pc2.bold("Usage:")}
|
|
640
|
+
npx @adhix11/shipguard [options]
|
|
641
|
+
|
|
642
|
+
${pc2.bold("Options:")}
|
|
643
|
+
--strict Treat warnings as errors (exit code 1)
|
|
644
|
+
--json Output results as JSON
|
|
645
|
+
--ignore <dirs> Comma-separated directories to ignore
|
|
646
|
+
(e.g., --ignore "dist,build,coverage")
|
|
647
|
+
--help, -h Show this help message
|
|
648
|
+
--version, -v Show version number
|
|
649
|
+
|
|
650
|
+
${pc2.bold("Examples:")}
|
|
651
|
+
npx @adhix11/shipguard
|
|
652
|
+
npx @adhix11/shipguard --strict
|
|
653
|
+
npx @adhix11/shipguard --json
|
|
654
|
+
npx @adhix11/shipguard --ignore "dist,node_modules,build"
|
|
655
|
+
`;
|
|
656
|
+
function parseArgs(argv) {
|
|
657
|
+
const args = argv.slice(2);
|
|
658
|
+
const options = {
|
|
659
|
+
strict: false,
|
|
660
|
+
json: false,
|
|
661
|
+
ignore: [],
|
|
662
|
+
help: false
|
|
663
|
+
};
|
|
664
|
+
for (let i = 0; i < args.length; i++) {
|
|
665
|
+
const arg = args[i];
|
|
666
|
+
switch (arg) {
|
|
667
|
+
case "--strict":
|
|
668
|
+
options.strict = true;
|
|
669
|
+
break;
|
|
670
|
+
case "--json":
|
|
671
|
+
options.json = true;
|
|
672
|
+
break;
|
|
673
|
+
case "--ignore": {
|
|
674
|
+
const next = args[++i];
|
|
675
|
+
if (next) {
|
|
676
|
+
options.ignore = next.split(",").map((s) => s.trim()).filter(Boolean);
|
|
677
|
+
}
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
case "--help":
|
|
681
|
+
case "-h":
|
|
682
|
+
options.help = true;
|
|
683
|
+
break;
|
|
684
|
+
case "--version":
|
|
685
|
+
case "-v":
|
|
686
|
+
console.log(VERSION);
|
|
687
|
+
process.exit(0);
|
|
688
|
+
break;
|
|
689
|
+
default:
|
|
690
|
+
if (arg.startsWith("--ignore=")) {
|
|
691
|
+
const value = arg.slice("--ignore=".length);
|
|
692
|
+
options.ignore = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
693
|
+
}
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return options;
|
|
698
|
+
}
|
|
699
|
+
async function main() {
|
|
700
|
+
const options = parseArgs(process.argv);
|
|
701
|
+
if (options.help) {
|
|
702
|
+
console.log(HELP_TEXT);
|
|
703
|
+
process.exit(0);
|
|
704
|
+
}
|
|
705
|
+
const cwd = resolve4(process.cwd());
|
|
706
|
+
if (!options.json) {
|
|
707
|
+
console.log();
|
|
708
|
+
console.log(
|
|
709
|
+
pc2.bold(pc2.cyan("\u{1F6A2} ShipGuard")) + pc2.dim(` v${VERSION}`) + pc2.dim(" \u2014 scanning project...")
|
|
710
|
+
);
|
|
711
|
+
console.log();
|
|
712
|
+
}
|
|
713
|
+
const files = await getFiles(cwd, options.ignore);
|
|
714
|
+
if (!options.json) {
|
|
715
|
+
console.log(pc2.dim(` Found ${files.length} files to scan`));
|
|
716
|
+
console.log();
|
|
717
|
+
}
|
|
718
|
+
const results = [];
|
|
719
|
+
results.push(scanSecrets(files));
|
|
720
|
+
results.push(await scanEnv(files, cwd));
|
|
721
|
+
results.push(scanDebug(files));
|
|
722
|
+
results.push(scanTests(files));
|
|
723
|
+
results.push(await scanPackageHealth(cwd));
|
|
724
|
+
printReport(results, options);
|
|
725
|
+
const exitCode = getExitCode(results, options);
|
|
726
|
+
process.exit(exitCode);
|
|
727
|
+
}
|
|
728
|
+
main().catch((err) => {
|
|
729
|
+
console.error(pc2.red(`
|
|
730
|
+
\u2717 ShipGuard encountered an error: ${err.message}`));
|
|
731
|
+
process.exit(2);
|
|
732
|
+
});
|
|
733
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/utils/readFiles.ts","../src/utils/report.ts","../src/scanners/secrets.ts","../src/scanners/env.ts","../src/scanners/debug.ts","../src/scanners/tests.ts","../src/scanners/packageHealth.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport pc from \"picocolors\";\nimport type { CLIOptions, ScanResult } from \"./types.js\";\nimport { getFiles } from \"./utils/readFiles.js\";\nimport { printReport, getExitCode } from \"./utils/report.js\";\nimport { scanSecrets } from \"./scanners/secrets.js\";\nimport { scanEnv } from \"./scanners/env.js\";\nimport { scanDebug } from \"./scanners/debug.js\";\nimport { scanTests } from \"./scanners/tests.js\";\nimport { scanPackageHealth } from \"./scanners/packageHealth.js\";\n\nconst VERSION = \"0.1.0\";\n\nconst HELP_TEXT = `\n${pc.bold(pc.cyan(\"๐ข ShipGuard\"))} v${VERSION}\n${pc.dim(\"Catch stupid production mistakes before you ship.\")}\n\n${pc.bold(\"Usage:\")}\n npx @adhix11/shipguard [options]\n\n${pc.bold(\"Options:\")}\n --strict Treat warnings as errors (exit code 1)\n --json Output results as JSON\n --ignore <dirs> Comma-separated directories to ignore\n (e.g., --ignore \"dist,build,coverage\")\n --help, -h Show this help message\n --version, -v Show version number\n\n${pc.bold(\"Examples:\")}\n npx @adhix11/shipguard\n npx @adhix11/shipguard --strict\n npx @adhix11/shipguard --json\n npx @adhix11/shipguard --ignore \"dist,node_modules,build\"\n`;\n\n/**\n * Parse CLI arguments into CLIOptions.\n */\nfunction parseArgs(argv: string[]): CLIOptions {\n const args = argv.slice(2);\n const options: CLIOptions = {\n strict: false,\n json: false,\n ignore: [],\n help: false,\n };\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n\n switch (arg) {\n case \"--strict\":\n options.strict = true;\n break;\n case \"--json\":\n options.json = true;\n break;\n case \"--ignore\": {\n const next = args[++i];\n if (next) {\n options.ignore = next.split(\",\").map((s) => s.trim()).filter(Boolean);\n }\n break;\n }\n case \"--help\":\n case \"-h\":\n options.help = true;\n break;\n case \"--version\":\n case \"-v\":\n console.log(VERSION);\n process.exit(0);\n break;\n default:\n if (arg.startsWith(\"--ignore=\")) {\n const value = arg.slice(\"--ignore=\".length);\n options.ignore = value.split(\",\").map((s) => s.trim()).filter(Boolean);\n }\n break;\n }\n }\n\n return options;\n}\n\n/**\n * Main entry point.\n */\nasync function main(): Promise<void> {\n const options = parseArgs(process.argv);\n\n if (options.help) {\n console.log(HELP_TEXT);\n process.exit(0);\n }\n\n const cwd = resolve(process.cwd());\n\n if (!options.json) {\n console.log();\n console.log(\n pc.bold(pc.cyan(\"๐ข ShipGuard\")) +\n pc.dim(` v${VERSION}`) +\n pc.dim(\" โ scanning project...\")\n );\n console.log();\n }\n\n // Load all scannable files\n const files = await getFiles(cwd, options.ignore);\n\n if (!options.json) {\n console.log(pc.dim(` Found ${files.length} files to scan`));\n console.log();\n }\n\n // Run all scanners\n const results: ScanResult[] = [];\n\n // 1. Secrets scan\n results.push(scanSecrets(files));\n\n // 2. Env scan\n results.push(await scanEnv(files, cwd));\n\n // 3. Debug code scan\n results.push(scanDebug(files));\n\n // 4. Test safety scan\n results.push(scanTests(files));\n\n // 5. Package health scan\n results.push(await scanPackageHealth(cwd));\n\n // Print report\n printReport(results, options);\n\n // Exit with appropriate code\n const exitCode = getExitCode(results, options);\n process.exit(exitCode);\n}\n\nmain().catch((err: Error) => {\n console.error(pc.red(`\\nโ ShipGuard encountered an error: ${err.message}`));\n process.exit(2);\n});\n","import fg from \"fast-glob\";\nimport { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\n\n/** Default directories/patterns to always ignore */\nconst DEFAULT_IGNORES = [\n \"node_modules\",\n \"dist\",\n \"build\",\n \".git\",\n \".next\",\n \".nuxt\",\n \"coverage\",\n \".cache\",\n \".turbo\",\n \".output\",\n \"out\",\n \"*.min.js\",\n \"*.min.css\",\n \"*.map\",\n \"package-lock.json\",\n \"yarn.lock\",\n \"pnpm-lock.yaml\",\n];\n\n/** File extensions we scan */\nconst SCAN_EXTENSIONS = [\n \"ts\",\n \"tsx\",\n \"js\",\n \"jsx\",\n \"mjs\",\n \"cjs\",\n \"json\",\n \"yaml\",\n \"yml\",\n \"env\",\n \"env.local\",\n \"env.development\",\n \"env.production\",\n \"env.test\",\n \"toml\",\n \"cfg\",\n \"conf\",\n \"ini\",\n];\n\nexport interface FileEntry {\n /** Relative path from project root */\n relativePath: string;\n /** Absolute path */\n absolutePath: string;\n /** File content */\n content: string;\n /** Lines of the file (split by newline) */\n lines: string[];\n}\n\n/**\n * Get all scannable files in the project directory.\n */\nexport async function getFiles(\n cwd: string,\n extraIgnores: string[] = []\n): Promise<FileEntry[]> {\n const ignorePatterns = [...DEFAULT_IGNORES, ...extraIgnores].map(\n (pattern) => `**/${pattern}/**`\n );\n\n const extensionGlob = `**/*.{${SCAN_EXTENSIONS.join(\",\")}}`;\n\n const paths = await fg(extensionGlob, {\n cwd,\n ignore: ignorePatterns,\n dot: true,\n absolute: false,\n onlyFiles: true,\n });\n\n const files: FileEntry[] = [];\n\n for (const relativePath of paths) {\n try {\n const absolutePath = resolve(cwd, relativePath);\n const content = await readFile(absolutePath, \"utf-8\");\n const lines = content.split(\"\\n\");\n files.push({ relativePath, absolutePath, content, lines });\n } catch {\n // Skip files that can't be read\n }\n }\n\n return files;\n}\n\n/**\n * Read a single file and return its content, or null if it doesn't exist.\n */\nexport async function readFileContent(\n filePath: string\n): Promise<string | null> {\n try {\n return await readFile(filePath, \"utf-8\");\n } catch {\n return null;\n }\n}\n","import pc from \"picocolors\";\nimport type { ScanResult, CLIOptions } from \"../types.js\";\n\n/**\n * Format and print the ShipGuard report to the terminal.\n */\nexport function printReport(results: ScanResult[], options: CLIOptions): void {\n if (options.json) {\n printJsonReport(results);\n return;\n }\n\n printFormattedReport(results, options);\n}\n\n/**\n * Returns the exit code based on scan results.\n * 0 = pass, 1 = fail\n */\nexport function getExitCode(\n results: ScanResult[],\n options: CLIOptions\n): number {\n const criticalCount = countBySeverity(results, \"critical\");\n const warningCount = countBySeverity(results, \"warning\");\n\n if (criticalCount > 0) return 1;\n if (options.strict && warningCount > 0) return 1;\n return 0;\n}\n\n// โโ Internal helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n\nfunction printJsonReport(results: ScanResult[]): void {\n const output = {\n tool: \"@adhix11/shipguard\",\n timestamp: new Date().toISOString(),\n results: results.map((r) => ({\n scanner: r.name,\n issues: r.issues.map((i) => ({\n file: i.file,\n line: i.line ?? null,\n message: i.message,\n severity: i.severity,\n })),\n })),\n summary: {\n critical: countBySeverity(results, \"critical\"),\n warnings: countBySeverity(results, \"warning\"),\n info: countBySeverity(results, \"info\"),\n totalIssues: results.reduce((sum, r) => sum + r.issues.length, 0),\n },\n };\n\n console.log(JSON.stringify(output, null, 2));\n}\n\nfunction printFormattedReport(\n results: ScanResult[],\n options: CLIOptions\n): void {\n console.log();\n console.log(\n pc.bold(pc.cyan(\"๐ข ShipGuard Report\"))\n );\n console.log(pc.dim(\"โ\".repeat(50)));\n console.log();\n\n let hasIssues = false;\n\n for (const result of results) {\n if (result.issues.length === 0) continue;\n hasIssues = true;\n\n // Group issues by severity to pick the header color\n const hasCritical = result.issues.some((i) => i.severity === \"critical\");\n const headerColor = hasCritical ? pc.red : pc.yellow;\n const icon = hasCritical ? \"โ\" : \"โ ๏ธ\";\n\n console.log(headerColor(pc.bold(`${icon} ${result.name}`)));\n\n // Group issues by file\n const byFile = groupByFile(result);\n\n for (const [file, issues] of Object.entries(byFile)) {\n console.log(pc.dim(` ${file}`));\n for (const issue of issues) {\n const lineInfo = issue.line ? pc.dim(` (line ${issue.line})`) : \"\";\n const bullet =\n issue.severity === \"critical\"\n ? pc.red(\" โ\")\n : issue.severity === \"warning\"\n ? pc.yellow(\" โ\")\n : pc.blue(\" โ\");\n console.log(`${bullet} ${issue.message}${lineInfo}`);\n }\n }\n\n console.log();\n }\n\n // Summary\n const criticalCount = countBySeverity(results, \"critical\");\n const warningCount = countBySeverity(results, \"warning\");\n const infoCount = countBySeverity(results, \"info\");\n\n console.log(pc.dim(\"โ\".repeat(50)));\n console.log(pc.bold(\"Summary:\"));\n\n if (criticalCount > 0) {\n console.log(pc.red(` ${criticalCount} critical issue${criticalCount !== 1 ? \"s\" : \"\"}`));\n }\n if (warningCount > 0) {\n console.log(pc.yellow(` ${warningCount} warning${warningCount !== 1 ? \"s\" : \"\"}`));\n }\n if (infoCount > 0) {\n console.log(pc.blue(` ${infoCount} info`));\n }\n\n console.log();\n\n const exitCode = getExitCode(results, options);\n\n if (!hasIssues) {\n console.log(pc.green(pc.bold(\"โ All clear! Ship with confidence. ๐\")));\n } else if (exitCode === 0) {\n console.log(\n pc.yellow(pc.bold(\"โ Warnings found. Review before shipping.\"))\n );\n } else {\n console.log(\n pc.red(pc.bold(\"โ Run failed. Fix critical issues before shipping.\"))\n );\n }\n\n console.log();\n}\n\nfunction countBySeverity(\n results: ScanResult[],\n severity: string\n): number {\n return results.reduce(\n (sum, r) => sum + r.issues.filter((i) => i.severity === severity).length,\n 0\n );\n}\n\nfunction groupByFile(\n result: ScanResult\n): Record<string, ScanResult[\"issues\"]> {\n const grouped: Record<string, ScanResult[\"issues\"]> = {};\n for (const issue of result.issues) {\n const key = issue.file || \"(project)\";\n if (!grouped[key]) grouped[key] = [];\n grouped[key].push(issue);\n }\n return grouped;\n}\n","import type { ScanResult, ScanIssue } from \"../types.js\";\nimport type { FileEntry } from \"../utils/readFiles.js\";\n\n/**\n * Patterns to detect hardcoded secrets.\n * Each pattern has a regex and a human-readable label.\n */\nconst SECRET_PATTERNS: { pattern: RegExp; label: string }[] = [\n {\n pattern: /AWS_ACCESS_KEY_ID\\s*[=:]\\s*['\"]?[A-Z0-9]{16,}/i,\n label: \"Possible AWS Access Key found\",\n },\n {\n pattern: /AWS_SECRET_ACCESS_KEY\\s*[=:]\\s*['\"]?[A-Za-z0-9/+=]{30,}/i,\n label: \"Possible AWS Secret Key found\",\n },\n {\n pattern: /AKIA[0-9A-Z]{16}/,\n label: \"AWS Access Key ID pattern detected\",\n },\n {\n pattern: /SECRET_KEY\\s*[=:]\\s*['\"]?[^\\s'\"]{8,}/i,\n label: \"Possible SECRET_KEY assignment found\",\n },\n {\n pattern: /PRIVATE_KEY\\s*[=:]\\s*['\"]?[^\\s'\"]{8,}/i,\n label: \"Possible PRIVATE_KEY assignment found\",\n },\n {\n pattern: /-----BEGIN\\s+(RSA\\s+)?PRIVATE\\s+KEY-----/,\n label: \"Private key block detected\",\n },\n {\n pattern: /mongodb\\+srv:\\/\\/[^\\s'\"]+/i,\n label: \"Possible MongoDB connection URI found\",\n },\n {\n pattern: /mongodb:\\/\\/[^\\s'\"]+/i,\n label: \"Possible MongoDB connection URI found\",\n },\n {\n pattern: /password\\s*[=:]\\s*['\"][^'\"]{4,}['\"]/i,\n label: \"Possible hardcoded password found\",\n },\n {\n pattern: /jwtSecret\\s*[=:]\\s*['\"][^'\"]+['\"]/i,\n label: \"Possible JWT secret found\",\n },\n {\n pattern: /jwt_secret\\s*[=:]\\s*['\"][^'\"]+['\"]/i,\n label: \"Possible JWT secret found\",\n },\n {\n pattern: /api[_-]?key\\s*[=:]\\s*['\"][^'\"]{8,}['\"]/i,\n label: \"Possible API key found\",\n },\n {\n pattern: /api[_-]?secret\\s*[=:]\\s*['\"][^'\"]{8,}['\"]/i,\n label: \"Possible API secret found\",\n },\n {\n pattern: /ghp_[A-Za-z0-9_]{36,}/,\n label: \"GitHub personal access token detected\",\n },\n {\n pattern: /sk-[A-Za-z0-9]{32,}/,\n label: \"Possible OpenAI/Stripe secret key detected\",\n },\n {\n pattern: /xox[bpras]-[A-Za-z0-9-]+/,\n label: \"Possible Slack token detected\",\n },\n];\n\n/** Files that are expected to have secrets-like patterns (skip them) */\nconst SKIP_PATTERNS = [\n /\\.env\\.example$/,\n /\\.env\\.sample$/,\n /\\.env\\.template$/,\n /package-lock\\.json$/,\n /yarn\\.lock$/,\n /pnpm-lock\\.yaml$/,\n];\n\n/**\n * Scan files for hardcoded secrets.\n */\nexport function scanSecrets(files: FileEntry[]): ScanResult {\n const issues: ScanIssue[] = [];\n\n for (const file of files) {\n // Skip files that are expected to contain template patterns\n if (SKIP_PATTERNS.some((p) => p.test(file.relativePath))) continue;\n\n for (let i = 0; i < file.lines.length; i++) {\n const line = file.lines[i];\n\n for (const { pattern, label } of SECRET_PATTERNS) {\n if (pattern.test(line)) {\n issues.push({\n file: file.relativePath,\n line: i + 1,\n message: label,\n severity: \"critical\",\n });\n break; // One match per line is enough\n }\n }\n }\n }\n\n return {\n name: \"Secrets Risk\",\n icon: \"๐\",\n issues,\n };\n}\n","import { resolve } from \"node:path\";\nimport type { ScanResult, ScanIssue } from \"../types.js\";\nimport type { FileEntry } from \"../utils/readFiles.js\";\nimport { readFileContent } from \"../utils/readFiles.js\";\n\n/** Regex to find process.env.VARIABLE_NAME usage */\nconst ENV_USAGE_PATTERN = /process\\.env\\.([A-Z_][A-Z0-9_]*)/g;\n\n/** Regex to find import.meta.env.VARIABLE_NAME usage (Vite) */\nconst VITE_ENV_PATTERN = /import\\.meta\\.env\\.([A-Z_][A-Z0-9_]*)/g;\n\n/** Standard env vars that don't need to be in .env.example */\nconst BUILTIN_ENV_VARS = new Set([\n \"NODE_ENV\",\n \"PORT\",\n \"HOME\",\n \"PATH\",\n \"USER\",\n \"SHELL\",\n \"TERM\",\n \"LANG\",\n \"PWD\",\n \"HOSTNAME\",\n \"CI\",\n \"TZ\",\n \"npm_package_version\",\n \"npm_package_name\",\n]);\n\n/**\n * Scan for environment variable usage and check against .env.example\n */\nexport async function scanEnv(\n files: FileEntry[],\n cwd: string\n): Promise<ScanResult> {\n const issues: ScanIssue[] = [];\n\n // Collect all env vars used in source code\n const usedEnvVars = new Map<string, { file: string; line: number }[]>();\n\n for (const file of files) {\n // Only scan source code files, not .env files themselves\n if (/\\.env/.test(file.relativePath)) continue;\n\n for (let i = 0; i < file.lines.length; i++) {\n const line = file.lines[i];\n\n for (const pattern of [ENV_USAGE_PATTERN, VITE_ENV_PATTERN]) {\n // Reset lastIndex because we reuse the regex\n pattern.lastIndex = 0;\n let match: RegExpExecArray | null;\n\n while ((match = pattern.exec(line)) !== null) {\n const varName = match[1];\n if (BUILTIN_ENV_VARS.has(varName)) continue;\n\n if (!usedEnvVars.has(varName)) {\n usedEnvVars.set(varName, []);\n }\n usedEnvVars.get(varName)!.push({\n file: file.relativePath,\n line: i + 1,\n });\n }\n }\n }\n }\n\n if (usedEnvVars.size === 0) {\n return { name: \"Missing Env Documentation\", icon: \"๐\", issues };\n }\n\n // Read .env.example\n const envExamplePath = resolve(cwd, \".env.example\");\n const envExampleContent = await readFileContent(envExamplePath);\n\n if (envExampleContent === null) {\n // No .env.example exists at all\n issues.push({\n file: \".env.example\",\n message: `.env.example file is missing. ${usedEnvVars.size} env variable${usedEnvVars.size !== 1 ? \"s\" : \"\"} found in code.`,\n severity: \"warning\",\n });\n\n for (const [varName] of usedEnvVars) {\n issues.push({\n file: \".env.example\",\n message: `Missing: ${varName}`,\n severity: \"warning\",\n });\n }\n } else {\n // Parse .env.example to get documented vars\n const documentedVars = new Set<string>();\n for (const line of envExampleContent.split(\"\\n\")) {\n const trimmed = line.trim();\n if (trimmed.startsWith(\"#\") || trimmed === \"\") continue;\n const eqIndex = trimmed.indexOf(\"=\");\n const varName = eqIndex !== -1 ? trimmed.slice(0, eqIndex).trim() : trimmed.trim();\n if (varName) documentedVars.add(varName);\n }\n\n // Find missing vars\n const missingVars: string[] = [];\n for (const [varName] of usedEnvVars) {\n if (!documentedVars.has(varName)) {\n missingVars.push(varName);\n }\n }\n\n if (missingVars.length > 0) {\n issues.push({\n file: \".env.example\",\n message: `.env.example is missing ${missingVars.length} variable${missingVars.length !== 1 ? \"s\" : \"\"}:`,\n severity: \"warning\",\n });\n\n for (const varName of missingVars) {\n issues.push({\n file: \".env.example\",\n message: `Missing: ${varName}`,\n severity: \"warning\",\n });\n }\n }\n }\n\n return { name: \"Missing Env Documentation\", icon: \"๐\", issues };\n}\n","import type { ScanResult, ScanIssue } from \"../types.js\";\nimport type { FileEntry } from \"../utils/readFiles.js\";\n\n/**\n * Debug/leftover patterns to detect.\n */\nconst DEBUG_PATTERNS: { pattern: RegExp; label: string }[] = [\n {\n pattern: /\\bconsole\\.log\\s*\\(/,\n label: \"console.log found\",\n },\n {\n pattern: /\\bconsole\\.debug\\s*\\(/,\n label: \"console.debug found\",\n },\n {\n pattern: /\\bconsole\\.warn\\s*\\(/,\n label: \"console.warn found\",\n },\n {\n pattern: /\\bdebugger\\b/,\n label: \"debugger statement found\",\n },\n {\n pattern: /\\/\\/\\s*TODO\\b/i,\n label: \"TODO comment found\",\n },\n {\n pattern: /\\/\\/\\s*FIXME\\b/i,\n label: \"FIXME comment found\",\n },\n {\n pattern: /\\/\\/\\s*HACK\\b/i,\n label: \"HACK comment found\",\n },\n {\n pattern: /\\/\\/\\s*XXX\\b/i,\n label: \"XXX marker found\",\n },\n {\n pattern: /\\balert\\s*\\(/,\n label: \"alert() call found\",\n },\n];\n\n/** Skip files that typically have console/debug usage by design */\nconst SKIP_PATTERNS = [\n /\\.config\\.(ts|js|mjs|cjs)$/,\n /tsup\\.config/,\n /vite\\.config/,\n /next\\.config/,\n /webpack\\.config/,\n /jest\\.config/,\n /eslint/,\n /prettier/,\n];\n\n/**\n * Scan for debug/leftover code that shouldn't ship to production.\n */\nexport function scanDebug(files: FileEntry[]): ScanResult {\n const issues: ScanIssue[] = [];\n\n for (const file of files) {\n // Skip config files\n if (SKIP_PATTERNS.some((p) => p.test(file.relativePath))) continue;\n\n // Skip non-source files\n if (!/\\.(ts|tsx|js|jsx|mjs|cjs)$/.test(file.relativePath)) continue;\n\n for (let i = 0; i < file.lines.length; i++) {\n const line = file.lines[i];\n\n // Skip commented-out lines that are just comments about these patterns\n // (i.e., \"// We removed console.log\" shouldn't trigger)\n\n for (const { pattern, label } of DEBUG_PATTERNS) {\n if (pattern.test(line)) {\n issues.push({\n file: file.relativePath,\n line: i + 1,\n message: label,\n severity: \"warning\",\n });\n }\n }\n }\n }\n\n return {\n name: \"Debug Code Found\",\n icon: \"๐\",\n issues,\n };\n}\n","import type { ScanResult, ScanIssue } from \"../types.js\";\nimport type { FileEntry } from \"../utils/readFiles.js\";\n\n/**\n * Risky test patterns that prevent proper test execution in CI.\n */\nconst TEST_RISK_PATTERNS: { pattern: RegExp; label: string }[] = [\n {\n pattern: /\\bdescribe\\.only\\s*\\(/,\n label: \"describe.only found โ other tests will be skipped\",\n },\n {\n pattern: /\\bit\\.only\\s*\\(/,\n label: \"it.only found โ other tests will be skipped\",\n },\n {\n pattern: /\\btest\\.only\\s*\\(/,\n label: \"test.only found โ other tests will be skipped\",\n },\n {\n pattern: /\\bdescribe\\.skip\\s*\\(/,\n label: \"describe.skip found โ tests are being skipped\",\n },\n {\n pattern: /\\bit\\.skip\\s*\\(/,\n label: \"it.skip found โ test is being skipped\",\n },\n {\n pattern: /\\btest\\.skip\\s*\\(/,\n label: \"test.skip found โ test is being skipped\",\n },\n {\n pattern: /\\bfdescribe\\s*\\(/,\n label: \"fdescribe found โ focused test suite (Jasmine)\",\n },\n {\n pattern: /\\bfit\\s*\\(/,\n label: \"fit found โ focused test (Jasmine)\",\n },\n {\n pattern: /\\bxit\\s*\\(/,\n label: \"xit found โ excluded test (Jasmine)\",\n },\n {\n pattern: /\\bxdescribe\\s*\\(/,\n label: \"xdescribe found โ excluded test suite (Jasmine)\",\n },\n];\n\n/** Only scan test files */\nconst TEST_FILE_PATTERN = /\\.(test|spec)\\.(ts|tsx|js|jsx|mjs|cjs)$/;\n\n/**\n * Scan test files for risky patterns (.only, .skip, focused tests).\n */\nexport function scanTests(files: FileEntry[]): ScanResult {\n const issues: ScanIssue[] = [];\n\n const testFiles = files.filter((f) => TEST_FILE_PATTERN.test(f.relativePath));\n\n for (const file of testFiles) {\n for (let i = 0; i < file.lines.length; i++) {\n const line = file.lines[i];\n\n for (const { pattern, label } of TEST_RISK_PATTERNS) {\n if (pattern.test(line)) {\n issues.push({\n file: file.relativePath,\n line: i + 1,\n message: label,\n severity: \"critical\",\n });\n }\n }\n }\n }\n\n return {\n name: \"Test Risk\",\n icon: \"๐งช\",\n issues,\n };\n}\n","import { resolve } from \"node:path\";\nimport { access, constants } from \"node:fs/promises\";\nimport type { ScanResult, ScanIssue } from \"../types.js\";\nimport { readFileContent } from \"../utils/readFiles.js\";\n\n/** Risky npm lifecycle scripts to flag */\nconst RISKY_SCRIPTS = [\"postinstall\", \"preinstall\", \"install\"];\n\n/** Shell command patterns that look suspicious in scripts */\nconst SUSPICIOUS_PATTERNS = [\n /curl\\s/,\n /wget\\s/,\n /sh\\s+-c/,\n /bash\\s+-c/,\n /powershell/i,\n /eval\\s/,\n /\\brm\\s+-rf/,\n />&\\s*\\/dev\\/null/,\n /\\.sh\\b/,\n];\n\n/**\n * Check package-level health: README, LICENSE, package.json fields, risky scripts.\n */\nexport async function scanPackageHealth(cwd: string): Promise<ScanResult> {\n const issues: ScanIssue[] = [];\n\n // Check README.md\n if (!(await fileExists(resolve(cwd, \"README.md\")))) {\n issues.push({\n file: \"README.md\",\n message: \"README.md is missing\",\n severity: \"warning\",\n });\n }\n\n // Check LICENSE\n if (!(await fileExists(resolve(cwd, \"LICENSE\")))) {\n issues.push({\n file: \"LICENSE\",\n message: \"LICENSE file is missing\",\n severity: \"warning\",\n });\n }\n\n // Check package.json\n const pkgContent = await readFileContent(resolve(cwd, \"package.json\"));\n\n if (!pkgContent) {\n issues.push({\n file: \"package.json\",\n message: \"package.json not found\",\n severity: \"critical\",\n });\n return { name: \"Package Health\", icon: \"๐ฆ\", issues };\n }\n\n let pkg: Record<string, unknown>;\n try {\n pkg = JSON.parse(pkgContent);\n } catch {\n issues.push({\n file: \"package.json\",\n message: \"package.json contains invalid JSON\",\n severity: \"critical\",\n });\n return { name: \"Package Health\", icon: \"๐ฆ\", issues };\n }\n\n // Required fields\n const requiredFields = [\"name\", \"version\", \"description\"];\n for (const field of requiredFields) {\n if (!pkg[field]) {\n issues.push({\n file: \"package.json\",\n message: `Missing required field: \"${field}\"`,\n severity: \"warning\",\n });\n }\n }\n\n // Should have main or bin or exports\n if (!pkg[\"main\"] && !pkg[\"bin\"] && !pkg[\"exports\"] && !pkg[\"module\"]) {\n issues.push({\n file: \"package.json\",\n message: 'Missing entry point: needs \"main\", \"bin\", \"module\", or \"exports\"',\n severity: \"warning\",\n });\n }\n\n // Check for risky scripts\n const scripts = pkg[\"scripts\"] as Record<string, string> | undefined;\n if (scripts) {\n for (const scriptName of RISKY_SCRIPTS) {\n const scriptValue = scripts[scriptName];\n if (!scriptValue) continue;\n\n // Check if the script has suspicious patterns\n const isSuspicious = SUSPICIOUS_PATTERNS.some((p) =>\n p.test(scriptValue)\n );\n\n if (isSuspicious) {\n issues.push({\n file: \"package.json\",\n message: `Risky lifecycle script \"${scriptName}\": ${scriptValue}`,\n severity: \"critical\",\n });\n } else {\n issues.push({\n file: \"package.json\",\n message: `Lifecycle script \"${scriptName}\" found: ${scriptValue}`,\n severity: \"info\",\n });\n }\n }\n }\n\n return { name: \"Package Health\", icon: \"๐ฆ\", issues };\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await access(filePath, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":["resolve","pc","resolve","resolve","SKIP_PATTERNS","resolve","resolve","pc","resolve"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adhix11/shipguard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A zero-config preflight scanner for JavaScript and TypeScript projects. Detect secrets, missing env docs, debug code, risky test flags, and basic package readiness before you ship.",
|
|
5
|
+
"author": "adhix11",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"shipguard": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"dev": "tsup --watch",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"preflight",
|
|
24
|
+
"scanner",
|
|
25
|
+
"secrets",
|
|
26
|
+
"security",
|
|
27
|
+
"lint",
|
|
28
|
+
"cli",
|
|
29
|
+
"devtools",
|
|
30
|
+
"shipguard",
|
|
31
|
+
"env",
|
|
32
|
+
"debug",
|
|
33
|
+
"test-safety",
|
|
34
|
+
"code-review",
|
|
35
|
+
"ai-code",
|
|
36
|
+
"release",
|
|
37
|
+
"pre-commit"
|
|
38
|
+
],
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/adhix11/shipguard"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/adhix11/shipguard/issues"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/adhix11/shipguard#readme",
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=16.0.0"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"fast-glob": "^3.3.3",
|
|
52
|
+
"picocolors": "^1.1.1"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^22.15.0",
|
|
56
|
+
"tsup": "^8.4.0",
|
|
57
|
+
"typescript": "^5.8.0"
|
|
58
|
+
}
|
|
59
|
+
}
|