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.
@@ -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 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)
89
+ const candidates = ctx.files.filter((f) => isCandidateFile(f.relPath));
90
+ if (candidates.length === 0)
32
91
  return [];
33
- let foundAny = false;
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 (content.toLowerCase().includes(h.toLowerCase())) {
40
- foundAny = true;
41
- break;
42
- }
98
+ if (lower.includes(h.toLowerCase()))
99
+ return [];
43
100
  }
44
- if (content.includes("helmet(") || content.includes('require("helmet")') || content.includes('from "helmet"')) {
45
- foundAny = true;
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.1.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 enriched = all.map((f) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shippingszn",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Read-only CLI scanner that checks a project for common pre-launch issues from the shippingszn.com launch checklist.",
5
5
  "license": "MIT",
6
6
  "type": "module",