shippingszn 0.1.0 → 0.3.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/dist/checks/headers.js +89 -33
- package/dist/index.js +30 -3
- package/dist/scan.js +21 -0
- package/package.json +1 -1
package/dist/checks/headers.js
CHANGED
|
@@ -7,48 +7,104 @@ const SECURITY_HEADER_NAMES = [
|
|
|
7
7
|
"X-Frame-Options",
|
|
8
8
|
"Referrer-Policy",
|
|
9
9
|
];
|
|
10
|
+
const CANDIDATE_EXTENSIONS = new Set([
|
|
11
|
+
".ts",
|
|
12
|
+
".tsx",
|
|
13
|
+
".js",
|
|
14
|
+
".jsx",
|
|
15
|
+
".mjs",
|
|
16
|
+
".cjs",
|
|
17
|
+
".mts",
|
|
18
|
+
".cts",
|
|
19
|
+
".json",
|
|
20
|
+
".toml",
|
|
21
|
+
]);
|
|
22
|
+
const CANDIDATE_BASENAMES = new Set([
|
|
23
|
+
"vite.config.ts",
|
|
24
|
+
"vite.config.js",
|
|
25
|
+
"vite.config.mjs",
|
|
26
|
+
"vite.config.cjs",
|
|
27
|
+
"next.config.js",
|
|
28
|
+
"next.config.mjs",
|
|
29
|
+
"next.config.ts",
|
|
30
|
+
"nuxt.config.ts",
|
|
31
|
+
"svelte.config.js",
|
|
32
|
+
"astro.config.mjs",
|
|
33
|
+
"astro.config.ts",
|
|
34
|
+
"vercel.json",
|
|
35
|
+
"netlify.toml",
|
|
36
|
+
]);
|
|
37
|
+
// Directories whose files commonly define server / middleware / route /
|
|
38
|
+
// header code. We scan every eligible file under these trees, not just a
|
|
39
|
+
// single "entry" file, so hand-rolled security-header middleware (e.g.
|
|
40
|
+
// a dedicated server/security.ts) is detected just as reliably as helmet().
|
|
41
|
+
const CANDIDATE_DIR_SEGMENTS = [
|
|
42
|
+
"server/",
|
|
43
|
+
"api/",
|
|
44
|
+
"backend/",
|
|
45
|
+
"middleware/",
|
|
46
|
+
"middlewares/",
|
|
47
|
+
"lib/server/",
|
|
48
|
+
"src/server/",
|
|
49
|
+
"src/api/",
|
|
50
|
+
"src/middleware/",
|
|
51
|
+
"src/middlewares/",
|
|
52
|
+
"apps/server/",
|
|
53
|
+
"apps/api/",
|
|
54
|
+
"packages/server/",
|
|
55
|
+
"packages/api/",
|
|
56
|
+
"artifacts/server/",
|
|
57
|
+
"artifacts/api-server/",
|
|
58
|
+
];
|
|
59
|
+
function isCandidateFile(relPath) {
|
|
60
|
+
const lower = relPath.replace(/\\/g, "/").toLowerCase();
|
|
61
|
+
if (lower.includes("/node_modules/") ||
|
|
62
|
+
lower.includes("/dist/") ||
|
|
63
|
+
lower.includes("/build/") ||
|
|
64
|
+
lower.includes("/.next/") ||
|
|
65
|
+
lower.endsWith(".test.ts") ||
|
|
66
|
+
lower.endsWith(".spec.ts") ||
|
|
67
|
+
lower.endsWith(".test.tsx") ||
|
|
68
|
+
lower.endsWith(".spec.tsx") ||
|
|
69
|
+
lower.endsWith(".test.js") ||
|
|
70
|
+
lower.endsWith(".spec.js")) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const base = path.basename(lower);
|
|
74
|
+
if (CANDIDATE_BASENAMES.has(base))
|
|
75
|
+
return true;
|
|
76
|
+
const ext = path.extname(lower);
|
|
77
|
+
if (!CANDIDATE_EXTENSIONS.has(ext))
|
|
78
|
+
return false;
|
|
79
|
+
if (/\b(?:server|index|app|main|bootstrap|middleware|security)\.(?:t|j|m|c)?sx?$/.test(base)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
for (const seg of CANDIDATE_DIR_SEGMENTS) {
|
|
83
|
+
if (lower.includes("/" + seg) || lower.startsWith(seg))
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
10
88
|
export async function checkSecurityHeaders(ctx) {
|
|
11
|
-
const
|
|
12
|
-
|
|
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)
|
|
89
|
+
const candidates = ctx.files.filter((f) => isCandidateFile(f.relPath));
|
|
90
|
+
if (candidates.length === 0)
|
|
32
91
|
return [];
|
|
33
|
-
|
|
34
|
-
for (const file of candidateFiles) {
|
|
92
|
+
for (const file of candidates) {
|
|
35
93
|
const content = await readFileSafe(file);
|
|
36
94
|
if (!content)
|
|
37
95
|
continue;
|
|
96
|
+
const lower = content.toLowerCase();
|
|
38
97
|
for (const h of SECURITY_HEADER_NAMES) {
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
break;
|
|
42
|
-
}
|
|
98
|
+
if (lower.includes(h.toLowerCase()))
|
|
99
|
+
return [];
|
|
43
100
|
}
|
|
44
|
-
if (
|
|
45
|
-
|
|
101
|
+
if (lower.includes("helmet(") ||
|
|
102
|
+
lower.includes('require("helmet")') ||
|
|
103
|
+
lower.includes('from "helmet"') ||
|
|
104
|
+
lower.includes("from 'helmet'")) {
|
|
105
|
+
return [];
|
|
46
106
|
}
|
|
47
|
-
if (foundAny)
|
|
48
|
-
break;
|
|
49
107
|
}
|
|
50
|
-
if (foundAny)
|
|
51
|
-
return [];
|
|
52
108
|
return [
|
|
53
109
|
{
|
|
54
110
|
checkId: "missing-security-headers",
|
package/dist/index.js
CHANGED
|
@@ -2,10 +2,35 @@
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as process from "node:process";
|
|
4
4
|
import { ALL_CHECKS } from "./checks.js";
|
|
5
|
-
import { listFiles } from "./scan.js";
|
|
5
|
+
import { listFiles, getTrackedFiles } from "./scan.js";
|
|
6
6
|
import { CHECKLIST_ITEMS, permalinkFor } from "./items.js";
|
|
7
|
+
const UNTRACKED_DOWNGRADE = {
|
|
8
|
+
critical: "lower",
|
|
9
|
+
high: "lower",
|
|
10
|
+
medium: "lower",
|
|
11
|
+
lower: "lower",
|
|
12
|
+
};
|
|
13
|
+
const UNTRACKED_SUFFIX = " (Note: this file is not tracked in git — it can't leak through a repo push, so severity is softened. If you ever commit it or ship it in a public artifact, re-scan.)";
|
|
14
|
+
function applyTrackingAwareSeverity(findings, tracked) {
|
|
15
|
+
if (!tracked)
|
|
16
|
+
return findings;
|
|
17
|
+
return findings.map((f) => {
|
|
18
|
+
if (!f.file)
|
|
19
|
+
return f;
|
|
20
|
+
if (tracked.has(f.file))
|
|
21
|
+
return f;
|
|
22
|
+
const nextSeverity = UNTRACKED_DOWNGRADE[f.severity];
|
|
23
|
+
if (nextSeverity === f.severity)
|
|
24
|
+
return f;
|
|
25
|
+
return {
|
|
26
|
+
...f,
|
|
27
|
+
severity: nextSeverity,
|
|
28
|
+
message: f.message + UNTRACKED_SUFFIX,
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
}
|
|
7
32
|
const DEFAULT_BASE_URL = "https://shippingszn.com";
|
|
8
|
-
const PKG_VERSION = "0.
|
|
33
|
+
const PKG_VERSION = "0.3.0";
|
|
9
34
|
function parseArgs(argv) {
|
|
10
35
|
const opts = {
|
|
11
36
|
cwd: process.cwd(),
|
|
@@ -89,6 +114,7 @@ async function run() {
|
|
|
89
114
|
}
|
|
90
115
|
const c = color(!opts.noColor && process.stdout.isTTY === true && !opts.json);
|
|
91
116
|
const files = await listFiles(opts.cwd);
|
|
117
|
+
const tracked = getTrackedFiles(opts.cwd);
|
|
92
118
|
const ctx = { rootDir: opts.cwd, files };
|
|
93
119
|
const all = [];
|
|
94
120
|
for (const check of ALL_CHECKS) {
|
|
@@ -106,7 +132,8 @@ async function run() {
|
|
|
106
132
|
});
|
|
107
133
|
}
|
|
108
134
|
}
|
|
109
|
-
const
|
|
135
|
+
const tracked_aware = applyTrackingAwareSeverity(all, tracked);
|
|
136
|
+
const enriched = tracked_aware.map((f) => {
|
|
110
137
|
const item = CHECKLIST_ITEMS[f.itemId];
|
|
111
138
|
return {
|
|
112
139
|
...f,
|
package/dist/scan.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
/**
|
|
5
|
+
* Ask git for the list of tracked files. Returns null when the directory
|
|
6
|
+
* isn't a git work tree (or git isn't available) — callers treat that as
|
|
7
|
+
* "don't know, assume tracked" so we never weaken a finding's severity
|
|
8
|
+
* on a non-git project.
|
|
9
|
+
*/
|
|
10
|
+
export function getTrackedFiles(rootDir) {
|
|
11
|
+
const result = spawnSync("git", ["-C", rootDir, "ls-files", "-z"], {
|
|
12
|
+
encoding: "utf8",
|
|
13
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
14
|
+
});
|
|
15
|
+
if (result.status !== 0 || !result.stdout)
|
|
16
|
+
return null;
|
|
17
|
+
const out = new Set();
|
|
18
|
+
for (const rel of result.stdout.split("\0")) {
|
|
19
|
+
if (rel)
|
|
20
|
+
out.add(rel);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
3
24
|
const DEFAULT_IGNORES = new Set([
|
|
4
25
|
"node_modules",
|
|
5
26
|
".git",
|
package/package.json
CHANGED