shippingszn 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 +93 -0
- package/dist/checks/dangerous.js +58 -0
- package/dist/checks/env.js +59 -0
- package/dist/checks/headers.js +60 -0
- package/dist/checks/helpers.js +155 -0
- package/dist/checks/index.js +34 -0
- package/dist/checks/language.js +275 -0
- package/dist/checks/public-assets.js +54 -0
- package/dist/checks/quality.js +45 -0
- package/dist/checks/secrets.js +271 -0
- package/dist/checks/types.js +1 -0
- package/dist/checks.js +8 -0
- package/dist/index.js +194 -0
- package/dist/items.js +46 -0
- package/dist/scan.js +171 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vibe Coder Launch Checklist contributors
|
|
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,93 @@
|
|
|
1
|
+
# shippingszn
|
|
2
|
+
|
|
3
|
+
A small, read-only CLI that scans a project for common pre-launch issues from
|
|
4
|
+
the [shippingszn.com launch checklist](https://shippingszn.com). Run it
|
|
5
|
+
before you ship to catch obvious mistakes — leaked API keys, missing
|
|
6
|
+
`robots.txt`, no security headers, and so on — and get a friendly report
|
|
7
|
+
linking each finding back to the matching checklist item.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx shippingszn
|
|
11
|
+
# or
|
|
12
|
+
pnpm dlx shippingszn
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Run it inside any project root. The CLI **never writes, modifies, or deletes**
|
|
16
|
+
any files — it only reads. Everything stays on your machine.
|
|
17
|
+
|
|
18
|
+
## What gets checked
|
|
19
|
+
|
|
20
|
+
The initial check set is intentionally small and high-signal. Each finding
|
|
21
|
+
maps back to one of the items on the checklist.
|
|
22
|
+
|
|
23
|
+
- Hardcoded API keys across many providers (OpenAI, Anthropic, Stripe, AWS,
|
|
24
|
+
Google, GitHub, Slack, private key blocks).
|
|
25
|
+
- `.env` present but not ignored in `.gitignore`, or `.env` present but no
|
|
26
|
+
`.env.example`.
|
|
27
|
+
- Missing `.gitignore`, `robots.txt`, `sitemap.xml`, or a custom favicon.
|
|
28
|
+
- Missing security-header middleware in common server configs.
|
|
29
|
+
- Dangerous code patterns: unsafe HTML injection in React, runtime
|
|
30
|
+
code-execution calls, wildcard CORS.
|
|
31
|
+
- Python: common debug-mode slip-ups, hardcoded framework secrets, missing
|
|
32
|
+
env-var loading.
|
|
33
|
+
- Ruby: unsafe string rendering, hardcoded Rails secrets.
|
|
34
|
+
- Go: `http.ListenAndServe` without TLS, hardcoded token / apiKey / secret
|
|
35
|
+
literals.
|
|
36
|
+
- Placeholder content (`lorem ipsum`, `John Doe`, `test@example.com`) and
|
|
37
|
+
`TODO` / `FIXME` / `XXX` / `HACK` comments.
|
|
38
|
+
|
|
39
|
+
Each finding is tagged Critical, High, Medium, or Lower and links back to the
|
|
40
|
+
relevant checklist item on shippingszn.com.
|
|
41
|
+
|
|
42
|
+
## What does NOT get checked
|
|
43
|
+
|
|
44
|
+
These are deliberately out of scope for v1:
|
|
45
|
+
|
|
46
|
+
- Anything that requires running your app (no live HTTP probing, no auth
|
|
47
|
+
flows).
|
|
48
|
+
- Auto-fixing problems. The CLI is read-only.
|
|
49
|
+
- Deep static analysis or language-specific lints. Use ESLint, Semgrep, or
|
|
50
|
+
Snyk for that.
|
|
51
|
+
- Validating your actual third-party dashboards (Stripe spend caps, OpenAI
|
|
52
|
+
quotas, etc.).
|
|
53
|
+
|
|
54
|
+
A clean report is **not** a launch certificate — it just means none of the
|
|
55
|
+
obvious things tripped a tripwire. Walk through the full checklist before you
|
|
56
|
+
ship.
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
shippingszn [path] [options]
|
|
62
|
+
|
|
63
|
+
Options:
|
|
64
|
+
--json Output a machine-readable JSON report.
|
|
65
|
+
--base-url <url> Base URL used to build links back to checklist items.
|
|
66
|
+
--cwd <path> Directory to scan. Default: current working directory.
|
|
67
|
+
--no-color Disable ANSI colors in the human-readable report.
|
|
68
|
+
-h, --help Show help.
|
|
69
|
+
-v, --version Print version.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Exit codes
|
|
73
|
+
|
|
74
|
+
- `0` — No critical findings.
|
|
75
|
+
- `1` — One or more critical findings detected.
|
|
76
|
+
- `2` — The scanner itself crashed.
|
|
77
|
+
|
|
78
|
+
This makes the CLI suitable for CI:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
# .github/workflows/launch-check.yml
|
|
82
|
+
- run: npx shippingszn --json > launch-check.json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Privacy
|
|
86
|
+
|
|
87
|
+
`shippingszn` reads files on your machine. It never uploads source code,
|
|
88
|
+
makes outbound network calls, or phones home. No telemetry. No accounts.
|
|
89
|
+
Inspect the source or audit `npm pack --dry-run` to confirm.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { isTextFile, readFileSafe } from "../scan.js";
|
|
3
|
+
import { findLine, isScanExempt, lineContainsIgnoreMarker, relPosix, } from "./helpers.js";
|
|
4
|
+
const DANGEROUS_PATTERNS = [
|
|
5
|
+
{
|
|
6
|
+
id: "dangerously-set-inner-html",
|
|
7
|
+
regex: /dangerouslySetInnerHTML/,
|
|
8
|
+
itemId: "common-attacks",
|
|
9
|
+
severity: "high",
|
|
10
|
+
message: "Use of dangerouslySetInnerHTML — make sure the content is sanitized or comes from a trusted source.",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "eval-call",
|
|
14
|
+
regex: /(^|[^A-Za-z0-9_$])eval\s*\(/,
|
|
15
|
+
itemId: "common-attacks",
|
|
16
|
+
severity: "high",
|
|
17
|
+
message: "Use of eval() — almost always avoidable and a common path to remote code execution if any input is user-controlled.",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "cors-wildcard",
|
|
21
|
+
regex: /Access-Control-Allow-Origin\s*[:=]\s*['"`]\*['"`]/i,
|
|
22
|
+
itemId: "common-attacks",
|
|
23
|
+
severity: "medium",
|
|
24
|
+
message: "Wildcard CORS (Access-Control-Allow-Origin: *). Lock this down to specific origins for any authenticated endpoint.",
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
export async function checkDangerousPatterns(ctx) {
|
|
28
|
+
const findings = [];
|
|
29
|
+
for (const file of ctx.files) {
|
|
30
|
+
if (!isTextFile(file))
|
|
31
|
+
continue;
|
|
32
|
+
if (isScanExempt(file.relPath))
|
|
33
|
+
continue;
|
|
34
|
+
const ext = path.extname(file.relPath).toLowerCase();
|
|
35
|
+
if (![".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".html"].includes(ext))
|
|
36
|
+
continue;
|
|
37
|
+
const content = await readFileSafe(file);
|
|
38
|
+
if (!content)
|
|
39
|
+
continue;
|
|
40
|
+
for (const pat of DANGEROUS_PATTERNS) {
|
|
41
|
+
const m = pat.regex.exec(content);
|
|
42
|
+
if (!m)
|
|
43
|
+
continue;
|
|
44
|
+
if (lineContainsIgnoreMarker(content, m.index))
|
|
45
|
+
continue;
|
|
46
|
+
const line = findLine(content, m.index);
|
|
47
|
+
findings.push({
|
|
48
|
+
checkId: pat.id,
|
|
49
|
+
itemId: pat.itemId,
|
|
50
|
+
severity: pat.severity,
|
|
51
|
+
message: pat.message,
|
|
52
|
+
file: relPosix(file.relPath),
|
|
53
|
+
line,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return findings;
|
|
58
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { fileExists, readFileSafe } from "../scan.js";
|
|
3
|
+
import { relPosix } from "./helpers.js";
|
|
4
|
+
export async function checkEnvCommitted(ctx) {
|
|
5
|
+
const findings = [];
|
|
6
|
+
const gitignorePath = path.join(ctx.rootDir, ".gitignore");
|
|
7
|
+
let gitignore = "";
|
|
8
|
+
if (await fileExists(gitignorePath)) {
|
|
9
|
+
gitignore = (await readFileSafe({ absPath: gitignorePath, relPath: ".gitignore", size: 0 })) ?? "";
|
|
10
|
+
}
|
|
11
|
+
const ignoresEnv = /^\s*\.env(\s|$)/m.test(gitignore) || /^\s*\*\.env(\s|$)/m.test(gitignore);
|
|
12
|
+
for (const file of ctx.files) {
|
|
13
|
+
const base = path.basename(file.relPath);
|
|
14
|
+
if (base !== ".env" && base !== ".env.local" && base !== ".env.production")
|
|
15
|
+
continue;
|
|
16
|
+
if (!ignoresEnv) {
|
|
17
|
+
findings.push({
|
|
18
|
+
checkId: "env-not-ignored",
|
|
19
|
+
itemId: "secrets",
|
|
20
|
+
severity: "high",
|
|
21
|
+
message: `${base} found and your .gitignore does not appear to ignore .env files.`,
|
|
22
|
+
file: relPosix(file.relPath),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return findings;
|
|
27
|
+
}
|
|
28
|
+
export async function checkEnvExample(ctx) {
|
|
29
|
+
const hasEnv = ctx.files.some((f) => path.basename(f.relPath) === ".env");
|
|
30
|
+
const hasExample = ctx.files.some((f) => {
|
|
31
|
+
const b = path.basename(f.relPath);
|
|
32
|
+
return b === ".env.example" || b === ".env.sample" || b === ".env.template";
|
|
33
|
+
});
|
|
34
|
+
if (hasEnv && !hasExample) {
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
checkId: "missing-env-example",
|
|
38
|
+
itemId: "secrets",
|
|
39
|
+
severity: "medium",
|
|
40
|
+
message: "Found a .env file but no .env.example. Add a sanitized .env.example so collaborators know which variables are required.",
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
export async function checkGitignore(ctx) {
|
|
47
|
+
const gitignorePath = path.join(ctx.rootDir, ".gitignore");
|
|
48
|
+
if (!(await fileExists(gitignorePath))) {
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
checkId: "missing-gitignore",
|
|
52
|
+
itemId: "github",
|
|
53
|
+
severity: "high",
|
|
54
|
+
message: "No .gitignore at the project root. Add one tuned to your stack so you don't accidentally commit secrets, local DBs, or build artifacts.",
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { readFileSafe } from "../scan.js";
|
|
3
|
+
const SECURITY_HEADER_NAMES = [
|
|
4
|
+
"Strict-Transport-Security",
|
|
5
|
+
"Content-Security-Policy",
|
|
6
|
+
"X-Content-Type-Options",
|
|
7
|
+
"X-Frame-Options",
|
|
8
|
+
"Referrer-Policy",
|
|
9
|
+
];
|
|
10
|
+
export async function checkSecurityHeaders(ctx) {
|
|
11
|
+
const candidateFiles = ctx.files.filter((f) => {
|
|
12
|
+
const rp = f.relPath.toLowerCase();
|
|
13
|
+
if (rp.endsWith(".test.ts") || rp.endsWith(".spec.ts"))
|
|
14
|
+
return false;
|
|
15
|
+
return (rp.endsWith("vite.config.ts") ||
|
|
16
|
+
rp.endsWith("vite.config.js") ||
|
|
17
|
+
rp.endsWith("next.config.js") ||
|
|
18
|
+
rp.endsWith("next.config.mjs") ||
|
|
19
|
+
rp.endsWith("next.config.ts") ||
|
|
20
|
+
rp.endsWith("nuxt.config.ts") ||
|
|
21
|
+
rp.endsWith("svelte.config.js") ||
|
|
22
|
+
rp.endsWith("astro.config.mjs") ||
|
|
23
|
+
rp.endsWith("astro.config.ts") ||
|
|
24
|
+
rp.endsWith("vercel.json") ||
|
|
25
|
+
rp.endsWith("netlify.toml") ||
|
|
26
|
+
/server\/index\.(t|j)s$/.test(rp) ||
|
|
27
|
+
/^server\.(t|j)s$/.test(path.basename(rp)) ||
|
|
28
|
+
/\bapp\.(t|j)s$/.test(rp) ||
|
|
29
|
+
/\bindex\.(t|j)s$/.test(rp));
|
|
30
|
+
});
|
|
31
|
+
if (candidateFiles.length === 0)
|
|
32
|
+
return [];
|
|
33
|
+
let foundAny = false;
|
|
34
|
+
for (const file of candidateFiles) {
|
|
35
|
+
const content = await readFileSafe(file);
|
|
36
|
+
if (!content)
|
|
37
|
+
continue;
|
|
38
|
+
for (const h of SECURITY_HEADER_NAMES) {
|
|
39
|
+
if (content.toLowerCase().includes(h.toLowerCase())) {
|
|
40
|
+
foundAny = true;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (content.includes("helmet(") || content.includes('require("helmet")') || content.includes('from "helmet"')) {
|
|
45
|
+
foundAny = true;
|
|
46
|
+
}
|
|
47
|
+
if (foundAny)
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
if (foundAny)
|
|
51
|
+
return [];
|
|
52
|
+
return [
|
|
53
|
+
{
|
|
54
|
+
checkId: "missing-security-headers",
|
|
55
|
+
itemId: "https-headers",
|
|
56
|
+
severity: "high",
|
|
57
|
+
message: "Couldn't find any common security headers (CSP, HSTS, X-Frame-Options, etc.) or helmet() middleware in your server/host configs. Add them so the browser enforces baseline defenses.",
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { fileExists, isTextFile, readFileSafe, } from "../scan.js";
|
|
3
|
+
export function relPosix(p) {
|
|
4
|
+
return p.split(path.sep).join("/");
|
|
5
|
+
}
|
|
6
|
+
export function findLine(content, idx) {
|
|
7
|
+
let line = 1;
|
|
8
|
+
for (let i = 0; i < idx; i++)
|
|
9
|
+
if (content.charCodeAt(i) === 10)
|
|
10
|
+
line++;
|
|
11
|
+
return line;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Inline opt-out marker. Any line containing this token is exempt from
|
|
15
|
+
* substring/regex-based checks (placeholder content, dangerous patterns).
|
|
16
|
+
* Used by the scanner's own source to avoid matching its pattern definitions.
|
|
17
|
+
*
|
|
18
|
+
* The literal value is split across a concatenation so that grep'ing for the
|
|
19
|
+
* marker only finds the *uses*, not this definition.
|
|
20
|
+
*/
|
|
21
|
+
export const IGNORE_MARKER = "shippingszn" + ":ignore";
|
|
22
|
+
export function lineContainsIgnoreMarker(content, charIndex) {
|
|
23
|
+
const lineStart = content.lastIndexOf("\n", charIndex - 1) + 1;
|
|
24
|
+
const lineEnd = content.indexOf("\n", charIndex);
|
|
25
|
+
const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
|
|
26
|
+
return line.includes(IGNORE_MARKER);
|
|
27
|
+
}
|
|
28
|
+
const PUBLIC_DIR_CANDIDATES = [
|
|
29
|
+
"public",
|
|
30
|
+
"static",
|
|
31
|
+
"www",
|
|
32
|
+
"dist",
|
|
33
|
+
"build",
|
|
34
|
+
"out",
|
|
35
|
+
];
|
|
36
|
+
export async function findPublicDirs(ctx) {
|
|
37
|
+
const dirs = [];
|
|
38
|
+
for (const cand of PUBLIC_DIR_CANDIDATES) {
|
|
39
|
+
const p = path.join(ctx.rootDir, cand);
|
|
40
|
+
if (await fileExists(p))
|
|
41
|
+
dirs.push(cand);
|
|
42
|
+
}
|
|
43
|
+
// Also any artifact public dirs in monorepo style.
|
|
44
|
+
const seen = new Set(dirs);
|
|
45
|
+
for (const f of ctx.files) {
|
|
46
|
+
const parts = f.relPath.split("/");
|
|
47
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
48
|
+
if (parts[i] === "public" || parts[i] === "static") {
|
|
49
|
+
const dir = parts.slice(0, i + 1).join("/");
|
|
50
|
+
if (!seen.has(dir)) {
|
|
51
|
+
seen.add(dir);
|
|
52
|
+
dirs.push(dir);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return dirs;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Narrow per-file exemption for substring/regex checks (placeholder
|
|
61
|
+
* content, dangerous patterns, language patterns).
|
|
62
|
+
*
|
|
63
|
+
* This deliberately does NOT exempt the CLI source or test trees as a
|
|
64
|
+
* whole — broad exemptions hide real bugs. It only lists files that
|
|
65
|
+
* define or document the patterns the scanner looks for, where a literal
|
|
66
|
+
* pattern in source is the file's whole purpose:
|
|
67
|
+
*
|
|
68
|
+
* - `tools/cli/src/checks/{dangerous,quality,language}.ts` define the
|
|
69
|
+
* regexes and human-readable messages, both of which contain the
|
|
70
|
+
* literal pattern strings.
|
|
71
|
+
* - `tools/cli/README.md` documents what the scanner detects, citing
|
|
72
|
+
* the pattern strings verbatim.
|
|
73
|
+
* - `tools/cli/test/fixtures/` contains intentional positive fixtures.
|
|
74
|
+
* - `artifacts/checklist/src/data/checklist/` is user-facing checklist
|
|
75
|
+
* copy that names the patterns by name (e.g. "look for TODO/FIXME"). shippingszn:ignore
|
|
76
|
+
*
|
|
77
|
+
* Secret scanning is NOT exempted from any of these paths — the secret
|
|
78
|
+
* regexes contain regex metacharacters in source and don't self-match,
|
|
79
|
+
* so real hardcoded secrets in the CLI source would still be caught.
|
|
80
|
+
*
|
|
81
|
+
* For one-off cases in normal source files, prefer the inline
|
|
82
|
+
* `shippingszn:ignore` marker (handled by lineContainsIgnoreMarker)
|
|
83
|
+
* instead of adding paths here.
|
|
84
|
+
*/
|
|
85
|
+
const PATTERN_DEFINITION_FILES = new Set([
|
|
86
|
+
"tools/cli/src/checks/dangerous.ts",
|
|
87
|
+
"tools/cli/src/checks/quality.ts",
|
|
88
|
+
"tools/cli/src/checks/language.ts",
|
|
89
|
+
"tools/cli/README.md",
|
|
90
|
+
]);
|
|
91
|
+
const PATTERN_DEFINITION_PREFIXES = [
|
|
92
|
+
"tools/cli/test/fixtures/",
|
|
93
|
+
"artifacts/checklist/src/data/checklist/",
|
|
94
|
+
];
|
|
95
|
+
export function isScanExempt(relPath) {
|
|
96
|
+
const p = relPosix(relPath);
|
|
97
|
+
if (PATTERN_DEFINITION_FILES.has(p))
|
|
98
|
+
return true;
|
|
99
|
+
for (const prefix of PATTERN_DEFINITION_PREFIXES) {
|
|
100
|
+
if (p.startsWith(prefix) || p.includes("/" + prefix))
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Does any source file in the project look like it dynamically emits or
|
|
107
|
+
* serves the given asset (e.g. `robots.txt`, `sitemap.xml`)? Used by
|
|
108
|
+
* `checkRobotsTxt` / `checkSitemapXml` to suppress false positives in
|
|
109
|
+
* projects that generate these at build time via a Vite plugin or serve
|
|
110
|
+
* them from an Express/Next route.
|
|
111
|
+
*
|
|
112
|
+
* Heuristic: a `.ts`/`.js`/`.mjs`/`.cjs` source file that mentions the
|
|
113
|
+
* asset filename as a string literal AND contains an emission/serve
|
|
114
|
+
* indicator (`emitFile`, `setHeader`, `res.end`, `res.send`,
|
|
115
|
+
* `configureServer`, `app.get`, `router.get`, `next/headers`).
|
|
116
|
+
*/
|
|
117
|
+
const SERVE_INDICATORS = [
|
|
118
|
+
"emitFile",
|
|
119
|
+
"setHeader",
|
|
120
|
+
"res.end",
|
|
121
|
+
"res.send",
|
|
122
|
+
"configureServer",
|
|
123
|
+
"configurePreviewServer",
|
|
124
|
+
"app.get",
|
|
125
|
+
"app.use",
|
|
126
|
+
"router.get",
|
|
127
|
+
"router.use",
|
|
128
|
+
];
|
|
129
|
+
const DYNAMIC_ASSET_SOURCE_EXTS = new Set([
|
|
130
|
+
".ts",
|
|
131
|
+
".tsx",
|
|
132
|
+
".js",
|
|
133
|
+
".jsx",
|
|
134
|
+
".mjs",
|
|
135
|
+
".cjs",
|
|
136
|
+
]);
|
|
137
|
+
export async function isAssetEmittedDynamically(ctx, assetName) {
|
|
138
|
+
const literalPatterns = [`"${assetName}"`, `'${assetName}'`, `/${assetName}`];
|
|
139
|
+
for (const file of ctx.files) {
|
|
140
|
+
if (!isTextFile(file))
|
|
141
|
+
continue;
|
|
142
|
+
const ext = path.extname(file.relPath).toLowerCase();
|
|
143
|
+
if (!DYNAMIC_ASSET_SOURCE_EXTS.has(ext))
|
|
144
|
+
continue;
|
|
145
|
+
const content = await readFileSafe(file);
|
|
146
|
+
if (!content)
|
|
147
|
+
continue;
|
|
148
|
+
if (!literalPatterns.some((p) => content.includes(p)))
|
|
149
|
+
continue;
|
|
150
|
+
if (!SERVE_INDICATORS.some((s) => content.includes(s)))
|
|
151
|
+
continue;
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry point for the scanner's check registry.
|
|
3
|
+
*
|
|
4
|
+
* Adding a new check: implement it in one of the per-domain modules
|
|
5
|
+
* (secrets, env, headers, dangerous, language, public-assets, quality)
|
|
6
|
+
* and append a `{ id, run }` entry to `ALL_CHECKS` below. The id is the
|
|
7
|
+
* stable identifier surfaced in CLI output and is what users would put
|
|
8
|
+
* in any future per-check disable list.
|
|
9
|
+
*/
|
|
10
|
+
import { checkHardcodedSecrets, checkConfigSecretLeaks } from "./secrets.js";
|
|
11
|
+
import { checkEnvCommitted, checkEnvExample, checkGitignore } from "./env.js";
|
|
12
|
+
import { checkRobotsTxt, checkSitemapXml, checkFavicon } from "./public-assets.js";
|
|
13
|
+
import { checkSecurityHeaders } from "./headers.js";
|
|
14
|
+
import { checkDangerousPatterns } from "./dangerous.js";
|
|
15
|
+
import { checkLanguagePatterns, checkPythonSecretKeyEnv, checkRubySecretKeyBaseEnv, } from "./language.js";
|
|
16
|
+
import { checkPlaceholderContent } from "./quality.js";
|
|
17
|
+
export * from "./types.js";
|
|
18
|
+
export { checkHardcodedSecrets, checkConfigSecretLeaks, checkEnvCommitted, checkEnvExample, checkGitignore, checkRobotsTxt, checkSitemapXml, checkFavicon, checkSecurityHeaders, checkDangerousPatterns, checkLanguagePatterns, checkPythonSecretKeyEnv, checkRubySecretKeyBaseEnv, checkPlaceholderContent, };
|
|
19
|
+
export const ALL_CHECKS = [
|
|
20
|
+
{ id: "hardcoded-secrets", run: checkHardcodedSecrets },
|
|
21
|
+
{ id: "config-secret-leaks", run: checkConfigSecretLeaks },
|
|
22
|
+
{ id: "env-committed", run: checkEnvCommitted },
|
|
23
|
+
{ id: "env-example", run: checkEnvExample },
|
|
24
|
+
{ id: "gitignore", run: checkGitignore },
|
|
25
|
+
{ id: "robots-txt", run: checkRobotsTxt },
|
|
26
|
+
{ id: "sitemap-xml", run: checkSitemapXml },
|
|
27
|
+
{ id: "favicon", run: checkFavicon },
|
|
28
|
+
{ id: "security-headers", run: checkSecurityHeaders },
|
|
29
|
+
{ id: "dangerous-patterns", run: checkDangerousPatterns },
|
|
30
|
+
{ id: "language-patterns", run: checkLanguagePatterns },
|
|
31
|
+
{ id: "python-secret-key-env", run: checkPythonSecretKeyEnv },
|
|
32
|
+
{ id: "ruby-secret-key-base-env", run: checkRubySecretKeyBaseEnv },
|
|
33
|
+
{ id: "placeholder-content", run: checkPlaceholderContent },
|
|
34
|
+
];
|