shipready 1.3.1 → 1.4.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.
@@ -1,8 +1,19 @@
1
+ import { isNodeEcosystem } from "../utils/framework.js";
1
2
  const IMPORTANT_SCRIPTS = ["dev", "build", "test", "lint"];
2
3
  /** Checks package.json existence and important scripts. */
3
4
  export function checkPackageJson(project) {
4
5
  const findings = [];
5
6
  if (!project.hasPackageJson) {
7
+ // Static sites, Python, Go, Rust, etc. don't need a package.json -
8
+ // penalizing them for it would be unfair noise.
9
+ if (!isNodeEcosystem(project.framework)) {
10
+ findings.push({
11
+ severity: "info",
12
+ rule: "package-json.not-applicable",
13
+ message: `package.json not required for a ${project.framework} project`,
14
+ });
15
+ return { name: "package.json", findings };
16
+ }
6
17
  findings.push({
7
18
  severity: "error",
8
19
  rule: "package-json.missing",
@@ -196,16 +196,19 @@ const PATTERNS = [
196
196
  {
197
197
  // Placeholder checks run against the password only (group 1), so
198
198
  // documentation hosts like db.example.com don't suppress real leaks.
199
+ // The host (group 2) downgrades localhost URLs to medium confidence.
199
200
  kind: "Database URL with password",
200
- re: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|rediss|amqp):\/\/[^:\s"'@/]+:([^@\s"']+)@/,
201
+ re: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|rediss|amqp):\/\/[^:\s"'@/]+:([^@\s"']+)@([^\s"'/:?]+)/,
201
202
  confidence: "high",
202
203
  group: 1,
204
+ hostGroup: 2,
203
205
  },
204
206
  {
205
207
  kind: "Basic auth in URL",
206
- re: /https?:\/\/[^:\s"'/@]{3,}:([^@\s"']{8,})@/,
208
+ re: /https?:\/\/[^:\s"'/@]{3,}:([^@\s"']{8,})@([^\s"'/:?]+)/,
207
209
  confidence: "medium",
208
210
  group: 1,
211
+ hostGroup: 2,
209
212
  minEntropy: 3.0,
210
213
  },
211
214
  {
@@ -240,6 +243,13 @@ const PLACEHOLDER_VALUE_RE = /your[-_ ]?|example|sample|placeholder|change[-_ ]?
240
243
  const PLACEHOLDER_LINE_RE = /\b(?:do not commit|for docs only|shipready-ignore)\b|<[A-Z_][A-Z0-9_ -]*>/i;
241
244
  /** Paths whose findings are downgraded to medium confidence. */
242
245
  const TEST_PATH_RE = /(^|[/\\])(tests?|__tests__|__mocks__|spec|specs|fixtures?|mocks?|examples?|samples?|docs?)([/\\]|$)|\.(test|spec)\.[a-z]+$/i;
246
+ /**
247
+ * Default/dev passwords that appear in scaffolding templates and docker
248
+ * compose files. A URL credential equal to one of these is not a leak.
249
+ */
250
+ const COMMON_DEV_PASSWORD_RE = /^(?:password|passwd|pass|root|admin|postgres|mysql|mongo|redis|rabbitmq|secret|test|dev|guest|user|toor|changeit|letmein|qwerty)$/i;
251
+ /** Hosts that indicate a local dev database, not a production leak. */
252
+ const LOCAL_HOST_RE = /^(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[?::1\]?|host\.docker\.internal|db|database|postgres|mysql|mongo|redis)$/i;
243
253
  /** True when a matched value looks like a placeholder or templated string. */
244
254
  function isPlaceholderValue(value) {
245
255
  if (PLACEHOLDER_VALUE_RE.test(value))
@@ -293,7 +303,16 @@ export function scanContentForSecrets(content, file, allowlist = []) {
293
303
  if (allowlist.some((a) => match[0].includes(a) || line.includes(a))) {
294
304
  continue;
295
305
  }
296
- const confidence = isTestPath ? "medium" : pattern.confidence;
306
+ let effective = pattern.confidence;
307
+ if (pattern.hostGroup !== undefined) {
308
+ // Scaffolding defaults like root:password@localhost are not leaks.
309
+ if (COMMON_DEV_PASSWORD_RE.test(value))
310
+ continue;
311
+ const host = match[pattern.hostGroup] ?? "";
312
+ if (LOCAL_HOST_RE.test(host))
313
+ effective = "medium";
314
+ }
315
+ const confidence = isTestPath ? "medium" : effective;
297
316
  found.push({
298
317
  kind: pattern.kind,
299
318
  file,
package/dist/scanner.js CHANGED
@@ -14,6 +14,8 @@ import { isRuleDisabled, loadConfig } from "./config.js";
14
14
  const CODE_ONLY = new Set([
15
15
  ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts",
16
16
  ".vue", ".svelte", ".py", ".rb", ".go", ".rs", ".java", ".php",
17
+ // Inline <script> in static sites carries console.log/TODO noise too
18
+ ".html", ".htm",
17
19
  ]);
18
20
  /** Gathers structured project info from the given root. */
19
21
  export async function detectProject(root, config) {
@@ -24,7 +26,7 @@ export async function detectProject(root, config) {
24
26
  hasPackageJson: fileExists(root, "package.json"),
25
27
  packageJson: pkg,
26
28
  packageManager: detectPackageManager(root),
27
- framework: detectFramework(pkg),
29
+ framework: detectFramework(pkg, root, sourceFiles),
28
30
  scripts: pkg?.scripts ?? {},
29
31
  sourceFiles,
30
32
  };
@@ -3,14 +3,34 @@ import path from "node:path";
3
3
  import fg from "fast-glob";
4
4
  /** Glob patterns that are always ignored when scanning. */
5
5
  export const IGNORE_PATTERNS = [
6
- "node_modules/**",
7
- ".git/**",
8
- "dist/**",
9
- "build/**",
10
- ".next/**",
11
- "out/**",
12
- "coverage/**",
13
- ".cache/**",
6
+ // Dependencies and VCS (at any depth)
7
+ "**/node_modules/**",
8
+ "**/.git/**",
9
+ // Build output (at any depth, e.g. mobile/build/)
10
+ "**/dist/**",
11
+ "**/build/**",
12
+ "**/.next/**",
13
+ "**/out/**",
14
+ "**/coverage/**",
15
+ "**/.cache/**",
16
+ "**/.turbo/**",
17
+ "**/.output/**",
18
+ // Minified/bundled vendor assets — not user code
19
+ "**/*.min.js",
20
+ "**/*.min.css",
21
+ "**/*.bundle.js",
22
+ "**/*.chunk.js",
23
+ "**/vendor/**",
24
+ "**/vendors/**",
25
+ // Python artifacts
26
+ "**/__pycache__/**",
27
+ "**/.venv/**",
28
+ "**/venv/**",
29
+ // Lockfiles and maps
30
+ "**/*.map",
31
+ "**/package-lock.json",
32
+ "**/pnpm-lock.yaml",
33
+ "**/yarn.lock",
14
34
  ];
15
35
  /** Extensions we treat as scannable text/code files. */
16
36
  export const CODE_EXTENSIONS = [
@@ -36,6 +56,9 @@ export const CODE_EXTENSIONS = [
36
56
  "toml",
37
57
  "env",
38
58
  "sh",
59
+ // Static sites: inline <script> blocks can leak keys just as easily
60
+ "html",
61
+ "htm",
39
62
  ];
40
63
  /** Returns true if the file exists (relative to root or absolute). */
41
64
  export function fileExists(root, rel) {
@@ -1,6 +1,7 @@
1
1
  import { fileExists } from "./files.js";
2
- /** Detects the package manager based on lockfiles. */
2
+ /** Detects the package manager based on lockfiles and manifests. */
3
3
  export function detectPackageManager(root) {
4
+ // Node ecosystems (lockfile wins over manifest)
4
5
  if (fileExists(root, "pnpm-lock.yaml"))
5
6
  return "pnpm";
6
7
  if (fileExists(root, "yarn.lock"))
@@ -9,34 +10,131 @@ export function detectPackageManager(root) {
9
10
  return "bun";
10
11
  if (fileExists(root, "package-lock.json"))
11
12
  return "npm";
13
+ if (fileExists(root, "deno.json") || fileExists(root, "deno.jsonc"))
14
+ return "deno";
15
+ // package.json without a lockfile is still an npm-family project
16
+ if (fileExists(root, "package.json"))
17
+ return "npm";
18
+ // Other ecosystems
19
+ if (fileExists(root, "uv.lock"))
20
+ return "uv";
21
+ if (fileExists(root, "poetry.lock"))
22
+ return "poetry";
23
+ if (fileExists(root, "requirements.txt") ||
24
+ fileExists(root, "pyproject.toml") ||
25
+ fileExists(root, "Pipfile")) {
26
+ return "pip";
27
+ }
28
+ if (fileExists(root, "Cargo.toml"))
29
+ return "cargo";
30
+ if (fileExists(root, "go.mod"))
31
+ return "go";
32
+ if (fileExists(root, "composer.json"))
33
+ return "composer";
34
+ if (fileExists(root, "Gemfile"))
35
+ return "bundler";
36
+ return "none";
37
+ }
38
+ /** Counts source files matching any of the given extensions. */
39
+ function countByExt(files, exts) {
40
+ return files.filter((f) => exts.some((e) => f.toLowerCase().endsWith(e))).length;
41
+ }
42
+ /**
43
+ * Detects the framework/project type. Checks package.json dependencies
44
+ * first, then falls back to manifest files and source file extensions so
45
+ * that static sites, Python, Go, Rust, PHP, and Ruby projects are
46
+ * identified instead of reported as "unknown".
47
+ */
48
+ export function detectFramework(pkg, root, sourceFiles = []) {
49
+ if (pkg) {
50
+ const deps = {
51
+ ...pkg.dependencies,
52
+ ...pkg.devDependencies,
53
+ };
54
+ // Order matters: meta-frameworks before their underlying libraries.
55
+ if (deps["next"])
56
+ return "Next.js";
57
+ if (deps["astro"])
58
+ return "Astro";
59
+ if (deps["@remix-run/react"] || deps["@remix-run/node"])
60
+ return "Remix";
61
+ if (deps["@angular/core"])
62
+ return "Angular";
63
+ if (deps["gatsby"])
64
+ return "Gatsby";
65
+ if (deps["@nestjs/core"])
66
+ return "NestJS";
67
+ if (deps["nuxt"] || deps["vue"])
68
+ return "Vue";
69
+ if (deps["svelte"] || deps["@sveltejs/kit"])
70
+ return "Svelte";
71
+ if (deps["vite"])
72
+ return "Vite";
73
+ if (deps["react"])
74
+ return "React";
75
+ if (deps["express"])
76
+ return "Express";
77
+ return "Node.js";
78
+ }
79
+ // No package.json: detect by manifests, then by dominant file type.
80
+ if (root) {
81
+ if (fileExists(root, "deno.json") || fileExists(root, "deno.jsonc"))
82
+ return "Deno";
83
+ if (fileExists(root, "pyproject.toml") ||
84
+ fileExists(root, "requirements.txt") ||
85
+ fileExists(root, "Pipfile")) {
86
+ return "Python";
87
+ }
88
+ if (fileExists(root, "Cargo.toml"))
89
+ return "Rust";
90
+ if (fileExists(root, "go.mod"))
91
+ return "Go";
92
+ if (fileExists(root, "composer.json"))
93
+ return "PHP";
94
+ if (fileExists(root, "Gemfile"))
95
+ return "Ruby";
96
+ if (fileExists(root, "pom.xml") || fileExists(root, "build.gradle") || fileExists(root, "build.gradle.kts")) {
97
+ return "Java";
98
+ }
99
+ }
100
+ const html = countByExt(sourceFiles, [".html", ".htm"]);
101
+ const py = countByExt(sourceFiles, [".py"]);
102
+ const go = countByExt(sourceFiles, [".go"]);
103
+ const rs = countByExt(sourceFiles, [".rs"]);
104
+ const php = countByExt(sourceFiles, [".php"]);
105
+ const rb = countByExt(sourceFiles, [".rb"]);
106
+ const js = countByExt(sourceFiles, [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"]);
107
+ const counts = [
108
+ ["Python", py],
109
+ ["Go", go],
110
+ ["Rust", rs],
111
+ ["PHP", php],
112
+ ["Ruby", rb],
113
+ ];
114
+ const [lang, max] = counts.reduce((a, b) => (b[1] > a[1] ? b : a), ["unknown", 0]);
115
+ // A language wins only when it clearly dominates over HTML/JS glue files.
116
+ if (max > 0 && max >= html && max >= js)
117
+ return lang;
118
+ // HTML pages with (or without) plain JS assets: a static site.
119
+ if (html > 0)
120
+ return "Static HTML";
121
+ if (js > 0)
122
+ return "Node.js";
12
123
  return "unknown";
13
124
  }
14
- /** Detects the framework from package.json dependencies. */
15
- export function detectFramework(pkg) {
16
- if (!pkg)
17
- return "unknown";
18
- const deps = {
19
- ...pkg.dependencies,
20
- ...pkg.devDependencies,
21
- };
22
- // Order matters: meta-frameworks before their underlying libraries.
23
- if (deps["next"])
24
- return "Next.js";
25
- if (deps["@nestjs/core"])
26
- return "NestJS";
27
- if (deps["nuxt"] || deps["vue"])
28
- return "Vue";
29
- if (deps["svelte"] || deps["@sveltejs/kit"])
30
- return "Svelte";
31
- if (deps["vite"] && deps["react"])
32
- return "Vite";
33
- if (deps["vite"])
34
- return "Vite";
35
- if (deps["react"])
36
- return "React";
37
- if (deps["express"])
38
- return "Express";
39
- return "Node.js";
125
+ /** True when the project belongs to the npm/Node ecosystem. */
126
+ export function isNodeEcosystem(framework) {
127
+ return ![
128
+ "Static HTML",
129
+ "Python",
130
+ "Go",
131
+ "Rust",
132
+ "PHP",
133
+ "Ruby",
134
+ "Java",
135
+ "Deno",
136
+ "unknown",
137
+ ].includes(framework);
40
138
  }
41
139
  /** Returns the install command for a given package manager. */
42
140
  export function installCommand(pm) {
@@ -47,8 +145,21 @@ export function installCommand(pm) {
47
145
  return "yarn install";
48
146
  case "bun":
49
147
  return "bun install";
50
- case "npm":
51
- case "unknown":
148
+ case "poetry":
149
+ return "poetry install";
150
+ case "uv":
151
+ return "uv sync";
152
+ case "pip":
153
+ return "pip install -r requirements.txt";
154
+ case "cargo":
155
+ return "cargo build";
156
+ case "go":
157
+ return "go mod download";
158
+ case "composer":
159
+ return "composer install";
160
+ case "bundler":
161
+ return "bundle install";
162
+ default:
52
163
  return "npm install";
53
164
  }
54
165
  }
@@ -61,8 +172,7 @@ export function runCommand(pm, script) {
61
172
  return `yarn ${script}`;
62
173
  case "bun":
63
174
  return `bun run ${script}`;
64
- case "npm":
65
- case "unknown":
175
+ default:
66
176
  return `npm run ${script}`;
67
177
  }
68
178
  }
@@ -120,11 +120,14 @@ export function renderReport(report, verbose = false, version) {
120
120
  const lines = [];
121
121
  const { project } = report;
122
122
  const title = pc.bold(pc.magenta("shipready")) + (version ? pc.dim(` v${version}`) : "");
123
+ const pmLabel = project.packageManager === "none" || project.packageManager === "unknown"
124
+ ? pc.dim("\u2014")
125
+ : project.packageManager;
123
126
  const meta = pc.dim("project ") +
124
127
  project.framework +
125
128
  pc.dim(" \u00b7 ") +
126
129
  pc.dim("pm ") +
127
- project.packageManager;
130
+ pmLabel;
128
131
  lines.push("");
129
132
  lines.push(...box([title, meta]));
130
133
  lines.push("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shipready",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Pre-flight check for your repo before shipping. Scans AI-coded projects for secrets, env issues, debug leftovers, and generates AI-agent instruction files.",
5
5
  "type": "module",
6
6
  "license": "MIT",