cleartoship 0.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.
- package/LICENSE +21 -0
- package/README.md +264 -0
- package/action.yml +154 -0
- package/dist/banner.d.ts +1 -0
- package/dist/banner.js +14 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +103 -0
- package/dist/data/popular.d.ts +17 -0
- package/dist/data/popular.js +103 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/report.d.ts +15 -0
- package/dist/report.js +317 -0
- package/dist/scan.d.ts +24 -0
- package/dist/scan.js +81 -0
- package/dist/scanners/community.d.ts +2 -0
- package/dist/scanners/community.js +155 -0
- package/dist/scanners/dependencies.d.ts +21 -0
- package/dist/scanners/dependencies.js +549 -0
- package/dist/scanners/index.d.ts +8 -0
- package/dist/scanners/index.js +13 -0
- package/dist/scanners/rls.d.ts +7 -0
- package/dist/scanners/rls.js +531 -0
- package/dist/scanners/secrets.d.ts +2 -0
- package/dist/scanners/secrets.js +439 -0
- package/dist/scanners/server-actions.d.ts +2 -0
- package/dist/scanners/server-actions.js +466 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +10 -0
- package/dist/utils/ast.d.ts +13 -0
- package/dist/utils/ast.js +107 -0
- package/dist/utils/detect.d.ts +2 -0
- package/dist/utils/detect.js +77 -0
- package/dist/utils/entropy.d.ts +7 -0
- package/dist/utils/entropy.js +19 -0
- package/dist/utils/files.d.ts +13 -0
- package/dist/utils/files.js +154 -0
- package/dist/utils/osv.d.ts +26 -0
- package/dist/utils/osv.js +123 -0
- package/dist/utils/registry.d.ts +44 -0
- package/dist/utils/registry.js +195 -0
- package/dist/utils/sql.d.ts +20 -0
- package/dist/utils/sql.js +157 -0
- package/dist/utils/suppress.d.ts +19 -0
- package/dist/utils/suppress.js +56 -0
- package/dist/utils/traverse.d.ts +2 -0
- package/dist/utils/traverse.js +14 -0
- package/dist/vendor/gitleaks/rules.d.ts +13 -0
- package/dist/vendor/gitleaks/rules.js +1571 -0
- package/dist/vendor/guardvibe/index.d.ts +11 -0
- package/dist/vendor/guardvibe/index.js +60 -0
- package/dist/vendor/guardvibe/rules/advanced-security.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/advanced-security.js +418 -0
- package/dist/vendor/guardvibe/rules/ai-host-security.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/ai-host-security.js +190 -0
- package/dist/vendor/guardvibe/rules/ai-security.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/ai-security.js +413 -0
- package/dist/vendor/guardvibe/rules/ai-tool-runtime.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/ai-tool-runtime.js +194 -0
- package/dist/vendor/guardvibe/rules/api-security.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/api-security.js +154 -0
- package/dist/vendor/guardvibe/rules/auth.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/auth.js +222 -0
- package/dist/vendor/guardvibe/rules/cicd.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/cicd.js +108 -0
- package/dist/vendor/guardvibe/rules/core.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/core.js +484 -0
- package/dist/vendor/guardvibe/rules/cve-versions.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/cve-versions.js +1130 -0
- package/dist/vendor/guardvibe/rules/database.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/database.js +173 -0
- package/dist/vendor/guardvibe/rules/deployment.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/deployment.js +269 -0
- package/dist/vendor/guardvibe/rules/dockerfile.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/dockerfile.js +99 -0
- package/dist/vendor/guardvibe/rules/firebase.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/firebase.js +96 -0
- package/dist/vendor/guardvibe/rules/go.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/go.js +85 -0
- package/dist/vendor/guardvibe/rules/modern-stack.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/modern-stack.js +631 -0
- package/dist/vendor/guardvibe/rules/nextjs.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/nextjs.js +233 -0
- package/dist/vendor/guardvibe/rules/other-services.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/other-services.js +72 -0
- package/dist/vendor/guardvibe/rules/payments.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/payments.js +123 -0
- package/dist/vendor/guardvibe/rules/react-native.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/react-native.js +132 -0
- package/dist/vendor/guardvibe/rules/services.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/services.js +149 -0
- package/dist/vendor/guardvibe/rules/shell.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/shell.js +72 -0
- package/dist/vendor/guardvibe/rules/sql.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/sql.js +60 -0
- package/dist/vendor/guardvibe/rules/supply-chain.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/supply-chain.js +243 -0
- package/dist/vendor/guardvibe/rules/terraform.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/terraform.js +84 -0
- package/dist/vendor/guardvibe/rules/types.d.ts +14 -0
- package/dist/vendor/guardvibe/rules/types.js +11 -0
- package/dist/vendor/guardvibe/rules/web-security.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/web-security.js +258 -0
- package/examples/security.yml +23 -0
- package/package.json +70 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Vendored from GuardVibe — https://github.com/goklab/guardvibe
|
|
3
|
+
* Copyright 2026 GokLab. Licensed under the Apache License, Version 2.0.
|
|
4
|
+
* Full licence: LICENSES/guardvibe-Apache-2.0.txt
|
|
5
|
+
*
|
|
6
|
+
* Modifications by ClearToShip: import paths rewritten for this package
|
|
7
|
+
* layout. Rule content is unchanged; rules that duplicate ClearToShip's own
|
|
8
|
+
* AST checks are disabled at runtime in src/scanners/community.ts rather
|
|
9
|
+
* than deleted here, so this file stays a faithful copy of upstream.
|
|
10
|
+
*/
|
|
11
|
+
// Security rules for Supabase, Prisma, and Drizzle ORM
|
|
12
|
+
export const databaseRules = [
|
|
13
|
+
{
|
|
14
|
+
id: "VG430",
|
|
15
|
+
name: "Supabase Anon Key on Server",
|
|
16
|
+
severity: "high",
|
|
17
|
+
owasp: "A01:2025 Broken Access Control",
|
|
18
|
+
description: "Using the anon/public key server-side with createClient bypasses Row Level Security. Use the service_role key, or use createServerClient from @supabase/ssr for cookie-based auth.",
|
|
19
|
+
pattern: /(?<!createServer)createClient\s*\([\s\S]{0,200}?(?:NEXT_PUBLIC_SUPABASE_ANON_KEY|supabaseAnonKey)(?![\s\S]{0,300}?cookies)/g,
|
|
20
|
+
languages: ["javascript", "typescript"],
|
|
21
|
+
fix: "Use SUPABASE_SERVICE_ROLE_KEY on the server, or use createServerClient from @supabase/ssr for cookie-based auth with the anon key.",
|
|
22
|
+
fixCode: '// Option 1: Service role key (admin access)\nconst supabase = createClient(\n process.env.SUPABASE_URL!,\n process.env.SUPABASE_SERVICE_ROLE_KEY!\n);\n\n// Option 2: SSR client with cookies (RLS-aware)\nimport { createServerClient } from "@supabase/ssr";\nconst supabase = createServerClient(url, anonKey, { cookies: { ... } });',
|
|
23
|
+
compliance: ["SOC2:CC6.6", "HIPAA:§164.312(a)"],
|
|
24
|
+
},
|
|
25
|
+
// VG431 removed — "Supabase Missing RLS Warning" triggered on every single
|
|
26
|
+
// supabase.from().select() call, creating extreme noise (1000+ hits in real projects).
|
|
27
|
+
// RLS is a database-level config, not detectable from application code patterns.
|
|
28
|
+
{
|
|
29
|
+
id: "VG432",
|
|
30
|
+
name: "Prisma Raw Query Injection (call-form)",
|
|
31
|
+
severity: "critical",
|
|
32
|
+
owasp: "A03:2025 Injection",
|
|
33
|
+
description: "Prisma $queryRaw / $executeRaw invoked as a function with a non-Prisma.sql backtick string argument. The tagged-template form (prisma.$queryRaw`...${var}...`) is auto-parameterized by Prisma and is safe; the call-form (prisma.$queryRaw(`...${var}...`)) interpolates the variable in JS first and bypasses parameterization. Prisma rejects the non-Prisma.sql call-form at runtime, but if it slipped past review it would be a critical injection vector.",
|
|
34
|
+
pattern: /\.\$(?:queryRaw|executeRaw)\s*\(\s*`[^`]*\$\{(?!Prisma\.)/g,
|
|
35
|
+
languages: ["javascript", "typescript"],
|
|
36
|
+
fix: "Use the tagged-template form (prisma.$queryRaw`SELECT ... ${var}`) or wrap the argument in Prisma.sql (prisma.$queryRaw(Prisma.sql`SELECT ... ${var}`)). Both are auto-parameterized.",
|
|
37
|
+
fixCode: "// Tagged template (preferred — auto-parameterized)\nconst rows = await prisma.$queryRaw`SELECT * FROM users WHERE id = ${userId}`;\n\n// Or with Prisma.sql wrapper\nimport { Prisma } from \"@prisma/client\";\nconst rows = await prisma.$queryRaw(Prisma.sql`SELECT * FROM users WHERE id = ${userId}`);",
|
|
38
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "VG433",
|
|
42
|
+
name: "Prisma queryRawUnsafe Usage",
|
|
43
|
+
severity: "critical",
|
|
44
|
+
owasp: "A03:2025 Injection",
|
|
45
|
+
description: "$queryRawUnsafe and $executeRawUnsafe pass raw SQL strings without parameterization. Extremely dangerous with user input.",
|
|
46
|
+
pattern: /\.\$(?:queryRawUnsafe|executeRawUnsafe)\s*\(/g,
|
|
47
|
+
languages: ["javascript", "typescript"],
|
|
48
|
+
fix: "Replace with $queryRaw using Prisma.sql tagged template.",
|
|
49
|
+
fixCode: "const result = await prisma.$queryRaw(\n Prisma.sql`SELECT * FROM users WHERE id = ${userId}`\n);",
|
|
50
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "VG434",
|
|
54
|
+
name: "Drizzle sql.raw() Injection",
|
|
55
|
+
severity: "critical",
|
|
56
|
+
owasp: "A03:2025 Injection",
|
|
57
|
+
description: "sql.raw() interpolated into an executed Drizzle query bypasses parameter binding, enabling SQL injection. Drizzle's sql`${value}` tag binds interpolations safely — only sql.raw() escapes that.",
|
|
58
|
+
pattern: /(?:db\.execute|db\.run|db\.get|db\.all)\s*\(\s*sql`[^`]*\$\{\s*sql\.raw\b/g,
|
|
59
|
+
languages: ["javascript", "typescript"],
|
|
60
|
+
fix: "Avoid sql.raw() with dynamic input. Use bound ${value} interpolation, or restrict raw fragments to a hardcoded allowlist of column/table identifiers.",
|
|
61
|
+
fixCode: 'import { sql } from "drizzle-orm";\n\nconst result = await db.execute(\n sql`SELECT * FROM users WHERE id = ${sql.placeholder("id")}`,\n { id: userId }\n);',
|
|
62
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "VG435",
|
|
66
|
+
name: "Database URL Client Exposure",
|
|
67
|
+
severity: "critical",
|
|
68
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
69
|
+
description: "DATABASE_URL or DIRECT_URL is accessed in client-side code. This exposes your database connection string to the browser.",
|
|
70
|
+
pattern: /["']use client["'][\s\S]{0,500}?process\.env\.(?:DATABASE_URL|DIRECT_URL)/g,
|
|
71
|
+
languages: ["javascript", "typescript"],
|
|
72
|
+
fix: "Never access database URLs in client components. Use Server Components or API routes.",
|
|
73
|
+
fixCode: "// Access database only server-side (no 'use client')\nexport default async function Page() {\n const data = await prisma.user.findMany();\n return <UserList users={data} />;\n}",
|
|
74
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req2.3", "HIPAA:§164.312(a)"],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "VG436",
|
|
78
|
+
name: "NEXT_PUBLIC Database URL",
|
|
79
|
+
severity: "critical",
|
|
80
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
81
|
+
description: "Database URL is prefixed with NEXT_PUBLIC_, exposing it in the client bundle.",
|
|
82
|
+
pattern: /NEXT_PUBLIC_\w*(?:DATABASE|DB|POSTGRES|MYSQL|MONGO|REDIS|SUPABASE_DB)\w*URL\s*=/gi,
|
|
83
|
+
languages: ["javascript", "typescript", "shell"],
|
|
84
|
+
fix: "Remove NEXT_PUBLIC_ prefix. Database URLs must only be server-side.",
|
|
85
|
+
fixCode: "# WRONG: exposed to client\n# NEXT_PUBLIC_DATABASE_URL=postgresql://...\n\n# CORRECT: server-side only\nDATABASE_URL=postgresql://...",
|
|
86
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req2.3", "HIPAA:§164.312(a)"],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "VG437",
|
|
90
|
+
name: "Supabase Service Role Key in Client",
|
|
91
|
+
severity: "critical",
|
|
92
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
93
|
+
description: "SUPABASE_SERVICE_ROLE_KEY is accessed in client-side code. This key bypasses RLS and grants full database access.",
|
|
94
|
+
pattern: /["']use client["'][\s\S]{0,500}?(?:SUPABASE_SERVICE_ROLE_KEY|SERVICE_ROLE)/g,
|
|
95
|
+
languages: ["javascript", "typescript"],
|
|
96
|
+
fix: "Never use the service role key in client code.",
|
|
97
|
+
fixCode: '// Server-side only\n"use server";\nconst adminClient = createClient(url, process.env.SUPABASE_SERVICE_ROLE_KEY!);',
|
|
98
|
+
compliance: ["SOC2:CC6.1", "HIPAA:§164.312(a)"],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "VG438",
|
|
102
|
+
name: "Supabase Public Storage Bucket",
|
|
103
|
+
severity: "high",
|
|
104
|
+
owasp: "A01:2025 Broken Access Control",
|
|
105
|
+
description: "Supabase storage bucket created with public: true. Without proper RLS policies, all files are accessible to anyone.",
|
|
106
|
+
pattern: /createBucket\s*\(\s*['"][^'"]+['"]\s*,\s*\{[\s\S]{0,200}?public\s*:\s*true/g,
|
|
107
|
+
languages: ["javascript", "typescript"],
|
|
108
|
+
fix: "Add storage RLS policies in Supabase Dashboard. Limit file types and sizes.",
|
|
109
|
+
fixCode: '// Create bucket with auth requirement\nconst { data } = await supabase.storage.createBucket("avatars", {\n public: false, // require auth\n fileSizeLimit: 5 * 1024 * 1024, // 5MB\n allowedMimeTypes: ["image/png", "image/jpeg"],\n});',
|
|
110
|
+
compliance: ["SOC2:CC6.1"],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "VG439",
|
|
114
|
+
name: "Postgres View Without SECURITY INVOKER",
|
|
115
|
+
severity: "high",
|
|
116
|
+
owasp: "A01:2025 Broken Access Control",
|
|
117
|
+
description: "PostgreSQL view is created without security_invoker = true. By default, views execute with the permissions of the view creator (SECURITY DEFINER), bypassing Row Level Security policies. In Supabase and any RLS-dependent system, this silently exposes all rows to any user who can query the view.",
|
|
118
|
+
pattern: /CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\s+(?:(?!security_invoker\s*=\s*true)[\s\S]){5,}?(?:AS\s+SELECT)/gi,
|
|
119
|
+
languages: ["sql"],
|
|
120
|
+
fix: "Add WITH (security_invoker = true) to all views that should respect RLS policies.",
|
|
121
|
+
fixCode: '-- GOOD: view respects RLS\nCREATE VIEW user_orders\n WITH (security_invoker = true)\n AS SELECT * FROM orders;\n\n-- BAD: bypasses RLS\n-- CREATE VIEW user_orders AS SELECT * FROM orders;',
|
|
122
|
+
compliance: ["SOC2:CC6.1"],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: "VG448",
|
|
126
|
+
name: "Supabase RPC Call May Bypass RLS",
|
|
127
|
+
severity: "high",
|
|
128
|
+
owasp: "A01:2025 Broken Access Control",
|
|
129
|
+
description: "supabase.rpc() calls execute PostgreSQL functions which may bypass Row Level Security if the function is defined as SECURITY DEFINER or if called with the service role key. Ensure the function uses SECURITY INVOKER or validate permissions manually.",
|
|
130
|
+
pattern: /supabase\.rpc\s*\(\s*["']\w+["']/g,
|
|
131
|
+
languages: ["javascript", "typescript"],
|
|
132
|
+
fix: "Ensure PostgreSQL functions called via rpc() use SECURITY INVOKER, or add explicit permission checks. Never call rpc() with the service role key from client-accessible code.",
|
|
133
|
+
fixCode: '-- PostgreSQL: use SECURITY INVOKER\nCREATE OR REPLACE FUNCTION get_user_data(user_id uuid)\nRETURNS TABLE (id uuid, name text)\nLANGUAGE sql\nSECURITY INVOKER -- respects RLS!\nAS $$\n SELECT id, name FROM users WHERE id = user_id;\n$$;\n\n// TypeScript: call with anon key (not service role)\nconst { data } = await supabase.rpc("get_user_data", { user_id: userId });',
|
|
134
|
+
compliance: ["SOC2:CC6.1"],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "VG1002",
|
|
138
|
+
name: "MongoDB NoSQL Injection via Query Operators",
|
|
139
|
+
severity: "high",
|
|
140
|
+
owasp: "A02:2025 Injection",
|
|
141
|
+
description: "MongoDB query operators ($where, $regex, $gt, $ne, $nin) used in database queries may be vulnerable to NoSQL injection if user input is passed directly without validation. Attackers can manipulate query logic to bypass authentication or extract data.",
|
|
142
|
+
pattern: /\.(find|findOne|updateOne|deleteOne|aggregate)\(\s*\{[^}]*(\$where|\$regex|\$gt|\$ne|\$nin)/g,
|
|
143
|
+
languages: ["javascript", "typescript"],
|
|
144
|
+
fix: "Validate and sanitize all user input before using it in MongoDB queries. Use a schema validation library (zod, joi) to ensure query parameters match expected types. Never pass raw request body directly to MongoDB queries.",
|
|
145
|
+
fixCode: 'import { z } from "zod";\n\n// Validate input before query\nconst schema = z.object({ id: z.string().regex(/^[a-f0-9]{24}$/) });\nconst { id } = schema.parse(req.body);\n\n// Safe query — no raw operators from user input\nconst user = await db.collection("users").findOne({ _id: new ObjectId(id) });',
|
|
146
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "VG1011",
|
|
150
|
+
name: "Drizzle sql.identifier() / .as() with User Input",
|
|
151
|
+
severity: "critical",
|
|
152
|
+
owasp: "A02:2025 Injection",
|
|
153
|
+
description: "Drizzle ORM's sql.identifier() and .as() accept raw strings for table/column names and aliases. Unlike sql`` tagged templates (which parameterize values), these functions interpolate directly into the SQL string. If user input reaches sql.identifier() or .as(), attackers can inject arbitrary SQL fragments — including UNION SELECT, subqueries, or DDL statements — bypassing ORM-level protections entirely.",
|
|
154
|
+
pattern: /(?:sql\.identifier|\.as)\s*\(\s*(?!["'`])[^)]*(?:req\.|params\.|query\.|body\.|input|args|user|ctx\.|formData|searchParams)/gi,
|
|
155
|
+
languages: ["javascript", "typescript"],
|
|
156
|
+
fix: "Never pass user input to sql.identifier() or .as(). Use a strict allowlist of valid table/column names and validate against it.",
|
|
157
|
+
fixCode: 'import { sql } from "drizzle-orm";\n\n// BAD: user input in identifier\nconst col = req.query.sortBy;\ndb.select().from(sql.identifier(col)); // SQL injection!\n\n// GOOD: allowlist valid identifiers\nconst ALLOWED_COLUMNS = ["name", "email", "created_at"] as const;\nconst col = ALLOWED_COLUMNS.find(c => c === req.query.sortBy);\nif (!col) throw new Error("Invalid column");\ndb.select().from(users).orderBy(users[col]);',
|
|
158
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: "VG1073",
|
|
162
|
+
name: "Drizzle sql.raw / sql.identifier with Interpolation or Concatenation (CVE-2026-39356 follow-on)",
|
|
163
|
+
severity: "critical",
|
|
164
|
+
owasp: "A02:2025 Injection",
|
|
165
|
+
description: "A sql.raw(...) or sql.identifier(...) call receives either a backtick template with ${...} interpolation or a string built with + concatenation. Both bypass Drizzle's parameterizer — the constructed string is fed straight into the executed query, exposing the same identifier-escape gap CVE-2026-39356 patched (drizzle-orm < 0.45.2 / 1.0.0-beta.20). On older builds it is a hard SQL injection; on patched builds sql.raw still has no escape at all. The supported safe shape is the tagged template `sql`... ${value}`` (auto-parameterized) plus a strict allowlist for any identifier the caller must vary. Distinct from VG1011, which catches the variable-name form sql.identifier(req.X) only; this rule covers the template-literal and string-concatenation shapes plus the previously-uncovered sql.raw call.",
|
|
166
|
+
pattern: /\bsql\s*\.\s*(?:raw|identifier)\s*\(\s*(?:`[^`]{0,400}\$\{|[\s\S]{0,200}?(?<![=!<>])\+(?!\+|=))/g,
|
|
167
|
+
languages: ["javascript", "typescript"],
|
|
168
|
+
fix: "Replace sql.raw / sql.identifier interpolation with the tagged-template form sql`... ${value}` (auto-parameterized) for values, and a hardcoded allowlist for any table/column identifier you must vary by request. Upgrade drizzle-orm to 0.45.2+ (stable) or 1.0.0-beta.20+ (beta) to close the escape gap covered by VG1052.",
|
|
169
|
+
fixCode: "// BAD — template-literal interpolation feeds attacker input straight in\nawait db.execute(sql.raw(`SELECT * FROM ${table} WHERE id = ${id}`));\n\n// BAD — string concatenation into sql.raw\nawait db.execute(sql.raw('SELECT * FROM users WHERE name = \\'' + name + '\\''));\n\n// GOOD — tagged template auto-parameterizes the value\nimport { sql } from 'drizzle-orm';\nawait db.execute(sql`SELECT * FROM users WHERE id = ${id}`);\n\n// GOOD — strict allowlist when the identifier really must vary\nconst ALLOWED_TABLES = ['users', 'orders'] as const;\nif (!ALLOWED_TABLES.includes(table as never)) throw new Error('Invalid table');\nawait db.execute(sql`SELECT * FROM ${sql.identifier(table)}`);",
|
|
170
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
171
|
+
exploit: "Attacker controls a request field that becomes the value of `table` or `id`. A payload like `users WHERE 1=1; DROP TABLE users; --` is concatenated into the raw query string; Drizzle hands the whole string to the driver and the driver executes the injected statements with the application's database role.",
|
|
172
|
+
},
|
|
173
|
+
];
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Vendored from GuardVibe — https://github.com/goklab/guardvibe
|
|
3
|
+
* Copyright 2026 GokLab. Licensed under the Apache License, Version 2.0.
|
|
4
|
+
* Full licence: LICENSES/guardvibe-Apache-2.0.txt
|
|
5
|
+
*
|
|
6
|
+
* Modifications by ClearToShip: import paths rewritten for this package
|
|
7
|
+
* layout. Rule content is unchanged; rules that duplicate ClearToShip's own
|
|
8
|
+
* AST checks are disabled at runtime in src/scanners/community.ts rather
|
|
9
|
+
* than deleted here, so this file stays a faithful copy of upstream.
|
|
10
|
+
*/
|
|
11
|
+
// Security rules for deployment config files
|
|
12
|
+
export const deploymentRules = [
|
|
13
|
+
// vercel.json / vercel.ts
|
|
14
|
+
{
|
|
15
|
+
id: "VG500",
|
|
16
|
+
name: "Vercel CORS Wildcard",
|
|
17
|
+
severity: "high",
|
|
18
|
+
owasp: "A01:2025 Broken Access Control",
|
|
19
|
+
description: "Vercel config sets Access-Control-Allow-Origin to wildcard (*), allowing any website to make requests.",
|
|
20
|
+
pattern: /["']Access-Control-Allow-Origin["'][\s\S]*?["']\*["']/g,
|
|
21
|
+
languages: ["vercel-config", "json"],
|
|
22
|
+
fix: "Restrict CORS to specific trusted origins.",
|
|
23
|
+
fixCode: '// vercel.json\n{\n "headers": [{\n "source": "/api/(.*)",\n "headers": [{ "key": "Access-Control-Allow-Origin", "value": "https://yourdomain.com" }]\n }]\n}',
|
|
24
|
+
compliance: ["SOC2:CC6.6"],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "VG501",
|
|
28
|
+
name: "Vercel Internal Rewrite Exposure",
|
|
29
|
+
severity: "high",
|
|
30
|
+
owasp: "A01:2025 Broken Access Control",
|
|
31
|
+
description: "Vercel rewrites expose internal service URLs to the public internet.",
|
|
32
|
+
pattern: /["']rewrites["']\s*:\s*\[[\s\S]*?["']destination["']\s*:\s*["']https?:\/\/(?:localhost|127\.0\.0\.1|10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.)/g,
|
|
33
|
+
languages: ["vercel-config", "json"],
|
|
34
|
+
fix: "Do not rewrite to internal network addresses. Use Vercel environment variables for service URLs.",
|
|
35
|
+
fixCode: '// Use environment variable for backend URL\n{\n "rewrites": [{\n "source": "/api/:path*",\n "destination": "https://api.yourdomain.com/:path*"\n }]\n}',
|
|
36
|
+
compliance: ["SOC2:CC6.6"],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "VG503",
|
|
40
|
+
name: "Vercel Cron Missing Secret Verification",
|
|
41
|
+
severity: "high",
|
|
42
|
+
owasp: "A01:2025 Broken Access Control",
|
|
43
|
+
description: "Cron jobs are configured but the endpoint may not verify CRON_SECRET. Anyone could trigger the cron endpoint manually.",
|
|
44
|
+
pattern: /["']crons["']\s*:\s*\[[\s\S]*?["']path["']\s*:\s*["'][^"']+["']/g,
|
|
45
|
+
languages: ["vercel-config", "json"],
|
|
46
|
+
fix: "Verify the CRON_SECRET header in your cron endpoint.",
|
|
47
|
+
fixCode: '// app/api/cron/route.ts\nexport async function GET(request: Request) {\n const authHeader = request.headers.get("authorization");\n if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {\n return new Response("Unauthorized", { status: 401 });\n }\n}',
|
|
48
|
+
compliance: ["SOC2:CC6.6"],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "VG504",
|
|
52
|
+
name: "Excessive Function Duration",
|
|
53
|
+
severity: "medium",
|
|
54
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
55
|
+
description: "Function maxDuration is set very high. Long-running functions increase costs and attack surface.",
|
|
56
|
+
pattern: /["']maxDuration["']\s*:\s*(?:[3-9]\d{2}|[1-9]\d{3,})/g,
|
|
57
|
+
languages: ["vercel-config", "json"],
|
|
58
|
+
fix: "Set maxDuration to the minimum required. Default 300s is sufficient for most use cases.",
|
|
59
|
+
fixCode: '// Set reasonable maxDuration\nexport const maxDuration = 60; // seconds — adjust to actual need',
|
|
60
|
+
compliance: ["SOC2:CC6.1"],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "VG506",
|
|
64
|
+
name: "Hardcoded Secret in Vercel Config",
|
|
65
|
+
severity: "critical",
|
|
66
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
67
|
+
description: "Secret or API key hardcoded in vercel.json. This file is committed to git.",
|
|
68
|
+
pattern: /["'](?:\w+_(?:SECRET|KEY|TOKEN|PASSWORD|CREDENTIAL)|(?:SECRET|PASSWORD|CREDENTIAL)_?\w*)["']\s*:\s*["'][A-Za-z0-9_\-]{12,}["']/gi,
|
|
69
|
+
languages: ["vercel-config", "json"],
|
|
70
|
+
fix: "Use Vercel environment variables (vercel env add) instead of hardcoding in config files.",
|
|
71
|
+
fixCode: '# Store secrets as Vercel env vars\nvercel env add SECRET_KEY production\n\n# Reference in code\nconst key = process.env.SECRET_KEY;',
|
|
72
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req2.3"],
|
|
73
|
+
},
|
|
74
|
+
// next.config
|
|
75
|
+
{
|
|
76
|
+
id: "VG507",
|
|
77
|
+
name: "Wildcard Remote Image Pattern",
|
|
78
|
+
severity: "high",
|
|
79
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
80
|
+
description: "next.config allows images from any hostname. This enables SSRF and hotlinking attacks.",
|
|
81
|
+
pattern: /remotePatterns\s*:\s*\[[\s\S]*?hostname\s*:\s*["'](?:\*\*|\*)["']/g,
|
|
82
|
+
languages: ["nextjs-config", "javascript", "typescript"],
|
|
83
|
+
fix: "Restrict remotePatterns to specific trusted hostnames.",
|
|
84
|
+
fixCode: '// next.config.ts\nimages: {\n remotePatterns: [\n { protocol: "https", hostname: "images.example.com" },\n ]\n}',
|
|
85
|
+
compliance: ["SOC2:CC6.6"],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "VG509",
|
|
89
|
+
name: "Powered By Header Enabled",
|
|
90
|
+
severity: "low",
|
|
91
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
92
|
+
description: "X-Powered-By header reveals Next.js framework. This helps attackers target known vulnerabilities.",
|
|
93
|
+
pattern: /poweredByHeader\s*:\s*true/g,
|
|
94
|
+
languages: ["nextjs-config", "javascript", "typescript"],
|
|
95
|
+
fix: "Set poweredByHeader to false in next.config.ts.",
|
|
96
|
+
fixCode: "// next.config.ts\nconst config = {\n poweredByHeader: false,\n};",
|
|
97
|
+
compliance: ["SOC2:CC6.1"],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "VG510",
|
|
101
|
+
name: "Next Config CORS Wildcard",
|
|
102
|
+
severity: "high",
|
|
103
|
+
owasp: "A01:2025 Broken Access Control",
|
|
104
|
+
description: "Next.js config headers() sets Access-Control-Allow-Origin to wildcard (*).",
|
|
105
|
+
pattern: /headers\s*\(\s*\)\s*\{[\s\S]*?Access-Control-Allow-Origin[\s\S]*?["']\*["']/g,
|
|
106
|
+
languages: ["nextjs-config", "javascript", "typescript"],
|
|
107
|
+
fix: "Restrict CORS to specific trusted origins.",
|
|
108
|
+
fixCode: '// Restrict to specific origins\nheaders: [\n { key: "Access-Control-Allow-Origin", value: "https://yourdomain.com" }\n]',
|
|
109
|
+
compliance: ["SOC2:CC6.6"],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "VG512",
|
|
113
|
+
name: "Source Maps in Production",
|
|
114
|
+
severity: "medium",
|
|
115
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
116
|
+
description: "Source maps enabled in production expose original source code to attackers.",
|
|
117
|
+
pattern: /devtool\s*:\s*["'](?:source-map|eval-source-map|cheap-source-map)["']/g,
|
|
118
|
+
languages: ["nextjs-config", "javascript", "typescript"],
|
|
119
|
+
fix: "Disable source maps in production.",
|
|
120
|
+
fixCode: "// next.config.ts\nconst config = {\n productionBrowserSourceMaps: false,\n};",
|
|
121
|
+
compliance: ["SOC2:CC6.1"],
|
|
122
|
+
},
|
|
123
|
+
// docker-compose.yml
|
|
124
|
+
{
|
|
125
|
+
id: "VG513",
|
|
126
|
+
name: "Docker Compose Public Port Binding",
|
|
127
|
+
severity: "medium",
|
|
128
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
129
|
+
description: "Port bound to 0.0.0.0 exposes the service to all network interfaces.",
|
|
130
|
+
pattern: /ports\s*:\s*\n\s*-\s*["']?0\.0\.0\.0:/gm,
|
|
131
|
+
languages: ["docker-compose", "yaml"],
|
|
132
|
+
fix: "Bind to 127.0.0.1 for local-only access.",
|
|
133
|
+
fixCode: '# Bind to localhost only\nports:\n - "127.0.0.1:3000:3000"',
|
|
134
|
+
compliance: ["SOC2:CC6.6"],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "VG514",
|
|
138
|
+
name: "Docker Compose Hardcoded Secret",
|
|
139
|
+
severity: "critical",
|
|
140
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
141
|
+
description: "Secret hardcoded in docker-compose environment section. Use Docker secrets or .env file.",
|
|
142
|
+
pattern: /environment\s*:\s*\n(?:\s*-\s*|\s*\w+\s*:\s*)[\s\S]*?(?:SECRET|PASSWORD|TOKEN|KEY|CREDENTIAL)\w*\s*[=:]\s*\S{8,}/gi,
|
|
143
|
+
languages: ["docker-compose", "yaml"],
|
|
144
|
+
fix: "Use Docker secrets or reference a .env file instead of hardcoding.",
|
|
145
|
+
fixCode: "# Use .env file\nservices:\n app:\n env_file:\n - .env",
|
|
146
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req2.3"],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "VG515",
|
|
150
|
+
name: "Docker Compose Privileged Container",
|
|
151
|
+
severity: "critical",
|
|
152
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
153
|
+
description: "Container runs in privileged mode with full host access. This is a severe security risk.",
|
|
154
|
+
pattern: /privileged\s*:\s*true/g,
|
|
155
|
+
languages: ["docker-compose", "yaml"],
|
|
156
|
+
fix: "Remove privileged: true. Use specific capabilities (cap_add) if needed.",
|
|
157
|
+
fixCode: "# Instead of privileged: true, use specific capabilities\nservices:\n app:\n cap_add:\n - NET_ADMIN",
|
|
158
|
+
compliance: ["SOC2:CC6.1"],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: "VG516",
|
|
162
|
+
name: "Docker Compose Host Volume Mount",
|
|
163
|
+
severity: "high",
|
|
164
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
165
|
+
description: "Mounting the entire host filesystem gives the container excessive access.",
|
|
166
|
+
pattern: /volumes\s*:\s*\n\s*-\s*["']?(?:\/:|\/etc|\/var|\/root|\/home)\s*:/gm,
|
|
167
|
+
languages: ["docker-compose", "yaml"],
|
|
168
|
+
fix: "Mount only specific directories needed by the application.",
|
|
169
|
+
fixCode: "# Mount only what's needed\nvolumes:\n - ./data:/app/data",
|
|
170
|
+
compliance: ["SOC2:CC6.1"],
|
|
171
|
+
},
|
|
172
|
+
// fly.toml / render.yaml / netlify.toml
|
|
173
|
+
{
|
|
174
|
+
id: "VG517",
|
|
175
|
+
name: "Platform Config Hardcoded Secret",
|
|
176
|
+
severity: "critical",
|
|
177
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
178
|
+
description: "Secret hardcoded in deployment platform config file. These files are committed to git.",
|
|
179
|
+
pattern: /\[env\][\s\S]*?(?:SECRET|PASSWORD|TOKEN|KEY|CREDENTIAL)\w*\s*=\s*["']?[A-Za-z0-9_\-]{12,}/gi,
|
|
180
|
+
languages: ["fly-config", "render-config", "netlify-config", "toml"],
|
|
181
|
+
fix: "Use your platform's secret management (fly secrets set, Render env groups, Netlify env vars).",
|
|
182
|
+
fixCode: '# fly.toml — don\'t put secrets here\n# Instead: fly secrets set SECRET_KEY=value\n\n[env]\n LOG_LEVEL = "info"',
|
|
183
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req2.3"],
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: "VG518",
|
|
187
|
+
name: "Platform Internal Port Exposed",
|
|
188
|
+
severity: "medium",
|
|
189
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
190
|
+
description: "Internal service port (database, cache) is exposed publicly.",
|
|
191
|
+
pattern: /internal_port\s*=\s*(?:5432|3306|6379|27017|9200|2379)/g,
|
|
192
|
+
languages: ["fly-config", "toml"],
|
|
193
|
+
fix: "Don't expose database or cache ports publicly. Use internal networking.",
|
|
194
|
+
fixCode: '# fly.toml — only expose your app port\n[[services]]\n internal_port = 3000 # app port only\n\n# Access database via internal Fly DNS\n# DATABASE_URL=postgres://db.internal:5432/mydb',
|
|
195
|
+
compliance: ["SOC2:CC6.6"],
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: "VG520",
|
|
199
|
+
name: "Missing HTTPS Redirect",
|
|
200
|
+
severity: "medium",
|
|
201
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
202
|
+
description: "No HTTP to HTTPS redirect configured. Traffic may be sent unencrypted.",
|
|
203
|
+
pattern: /force_https\s*=\s*false/g,
|
|
204
|
+
languages: ["fly-config", "toml"],
|
|
205
|
+
fix: "Enable force_https to redirect all HTTP traffic to HTTPS.",
|
|
206
|
+
fixCode: '# fly.toml\n[[services]]\n [services.concurrency]\n hard_limit = 25\n [[services.ports]]\n force_https = true\n port = 80',
|
|
207
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req4.1"],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: "VG521",
|
|
211
|
+
name: "Kubernetes Privileged Container",
|
|
212
|
+
severity: "critical",
|
|
213
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
214
|
+
description: "Kubernetes pod runs with privileged: true, granting full access to the host kernel. This is the #1 container escape vector — a compromised pod can access all host devices, mount the host filesystem, and take over the entire node.",
|
|
215
|
+
pattern: /securityContext:[\s\S]{0,100}?privileged:\s*true/gi,
|
|
216
|
+
languages: ["yaml"],
|
|
217
|
+
fix: "Remove privileged: true. Use specific Linux capabilities if needed instead of full privilege escalation.",
|
|
218
|
+
fixCode: '# BAD: full host access\n# securityContext:\n# privileged: true\n\n# GOOD: drop all capabilities, add only what\'s needed\nsecurityContext:\n privileged: false\n runAsNonRoot: true\n capabilities:\n drop: ["ALL"]',
|
|
219
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req6.5.10"],
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: "VG522",
|
|
223
|
+
name: "Kubernetes Secrets in ConfigMap",
|
|
224
|
+
severity: "critical",
|
|
225
|
+
owasp: "A01:2025 Broken Access Control",
|
|
226
|
+
description: "Sensitive data (passwords, tokens, API keys, credentials) stored in a Kubernetes ConfigMap instead of a Secret. ConfigMaps are not encrypted at rest and are visible to anyone with basic cluster read access.",
|
|
227
|
+
pattern: /kind:\s*ConfigMap[\s\S]{0,500}?(?:password|secret|token|api[_-]?key|credential|private[_-]?key|database[_-]?url)\s*:\s*\S+/gi,
|
|
228
|
+
languages: ["yaml"],
|
|
229
|
+
fix: "Move sensitive data to Kubernetes Secrets with encryption at rest enabled. Never store credentials in ConfigMaps.",
|
|
230
|
+
fixCode: '# BAD: secret in ConfigMap\n# kind: ConfigMap\n# data:\n# database-password: "s3cret"\n\n# GOOD: use a Secret\napiVersion: v1\nkind: Secret\nmetadata:\n name: db-credentials\ntype: Opaque\nstringData:\n database-password: "s3cret"',
|
|
231
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req2.3", "HIPAA:§164.312(a)"],
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: "VG523",
|
|
235
|
+
name: "Kubernetes Host Namespace Sharing",
|
|
236
|
+
severity: "critical",
|
|
237
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
238
|
+
description: "Pod uses hostNetwork, hostPID, or hostIPC. This breaks container isolation — the pod can see all host processes, sniff network traffic, and communicate with host-level IPC, enabling trivial container escapes.",
|
|
239
|
+
pattern: /(?:hostNetwork|hostPID|hostIPC):\s*true/gi,
|
|
240
|
+
languages: ["yaml"],
|
|
241
|
+
fix: "Remove hostNetwork, hostPID, and hostIPC unless absolutely required (e.g., CNI plugins). Use NetworkPolicies for inter-pod communication.",
|
|
242
|
+
fixCode: '# BAD: breaks container isolation\n# hostNetwork: true\n# hostPID: true\n\n# GOOD: use pod networking\nspec:\n hostNetwork: false\n hostPID: false\n hostIPC: false\n containers:\n - name: app\n securityContext:\n runAsNonRoot: true',
|
|
243
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req6.5.10"],
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: "VG524",
|
|
247
|
+
name: "Data URL or Blob URL in User-Controlled src/href",
|
|
248
|
+
severity: "high",
|
|
249
|
+
owasp: "A07:2025 Cross-Site Scripting",
|
|
250
|
+
description: "User-controlled input is used in src or href attributes without blocking data: and blob: URL schemes. data:text/html URLs can embed full HTML pages with scripts, and javascript: URLs execute code — bypassing same-origin restrictions.",
|
|
251
|
+
pattern: /(?:src|href|action|srcDoc)\s*=\s*\{[^}]*?(?:data\s*:|blob\s*:|javascript\s*:)[^}]*\}/gi,
|
|
252
|
+
languages: ["javascript", "typescript"],
|
|
253
|
+
fix: "Validate URL scheme against an allowlist (https: only). Block data:, blob:, and javascript: URLs from user input.",
|
|
254
|
+
fixCode: '// Validate URL scheme\nfunction isSafeUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n return ["https:", "http:"].includes(parsed.protocol);\n } catch {\n return false;\n }\n}\n\n// Usage\n<img src={isSafeUrl(userUrl) ? userUrl : "/placeholder.png"} />',
|
|
255
|
+
compliance: ["SOC2:CC7.1"],
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
id: "VG1001",
|
|
259
|
+
name: "Kubernetes Secret Hardcoded Value",
|
|
260
|
+
severity: "critical",
|
|
261
|
+
owasp: "A07:2025 Identification and Authentication Failures",
|
|
262
|
+
description: "Kubernetes Secret manifest contains hardcoded base64-encoded values in the data field. These secrets are only base64-encoded (not encrypted) and will be exposed in version control. Use External Secrets, Sealed Secrets, or a secrets manager instead.",
|
|
263
|
+
pattern: /kind:\s*Secret[\s\S]*?data:[\s\S]*?(?!\s*\{\{)[\w+/=]{8,}/g,
|
|
264
|
+
languages: ["yaml"],
|
|
265
|
+
fix: "Never commit Secret manifests with hardcoded values. Use External Secrets Operator, Sealed Secrets, or inject secrets via CI/CD pipeline. Store sensitive values in a secrets manager (Vault, AWS Secrets Manager, etc.).",
|
|
266
|
+
fixCode: '# Use External Secrets Operator\napiVersion: external-secrets.io/v1beta1\nkind: ExternalSecret\nmetadata:\n name: my-secret\nspec:\n refreshInterval: 1h\n secretStoreRef:\n name: vault-backend\n kind: SecretStore\n target:\n name: my-secret\n data:\n - secretKey: password\n remoteRef:\n key: secret/data/myapp\n property: password',
|
|
267
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req6.5.3", "HIPAA:§164.312(a)"],
|
|
268
|
+
},
|
|
269
|
+
];
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Vendored from GuardVibe — https://github.com/goklab/guardvibe
|
|
3
|
+
* Copyright 2026 GokLab. Licensed under the Apache License, Version 2.0.
|
|
4
|
+
* Full licence: LICENSES/guardvibe-Apache-2.0.txt
|
|
5
|
+
*
|
|
6
|
+
* Modifications by ClearToShip: import paths rewritten for this package
|
|
7
|
+
* layout. Rule content is unchanged; rules that duplicate ClearToShip's own
|
|
8
|
+
* AST checks are disabled at runtime in src/scanners/community.ts rather
|
|
9
|
+
* than deleted here, so this file stays a faithful copy of upstream.
|
|
10
|
+
*/
|
|
11
|
+
export const dockerfileRules = [
|
|
12
|
+
{
|
|
13
|
+
id: "VG200",
|
|
14
|
+
name: "Container running as root",
|
|
15
|
+
severity: "high",
|
|
16
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
17
|
+
description: "Dockerfile has no USER instruction. Container will run as root, increasing attack surface.",
|
|
18
|
+
pattern: /^FROM\s+\S+(?:(?!^USER\s)[\s\S])*$/gim,
|
|
19
|
+
languages: ["dockerfile"],
|
|
20
|
+
fix: "Add a USER instruction after installing dependencies: USER node (or appropriate non-root user).",
|
|
21
|
+
fixCode: "FROM node:20-alpine\nRUN addgroup -S app && adduser -S app -G app\n# ... install dependencies ...\nUSER app\nCMD [\"node\", \"server.js\"]",
|
|
22
|
+
compliance: ["SOC2:CC6.1"],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "VG201",
|
|
26
|
+
name: "COPY all before dependency install",
|
|
27
|
+
severity: "medium",
|
|
28
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
29
|
+
description: "COPY . . before RUN install copies everything including secrets into the image layer history.",
|
|
30
|
+
pattern: /COPY\s+\.\s+\.\s*\n[\s\S]*?RUN\s+.*(?:npm|pip|go|bundle|cargo)\s+install/gi,
|
|
31
|
+
languages: ["dockerfile"],
|
|
32
|
+
fix: "Copy only dependency files first (package.json, requirements.txt), then install, then copy source.",
|
|
33
|
+
fixCode: "# Copy dependency files first\nCOPY package.json package-lock.json ./\nRUN npm ci --production\n# Then copy source\nCOPY . .",
|
|
34
|
+
compliance: ["SOC2:CC6.1"],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "VG202",
|
|
38
|
+
name: "Using latest or untagged image",
|
|
39
|
+
severity: "medium",
|
|
40
|
+
owasp: "A03:2025 Software Supply Chain Failures",
|
|
41
|
+
description: "Using :latest tag or no tag makes builds non-reproducible and vulnerable to supply chain attacks.",
|
|
42
|
+
pattern: /^FROM\s+[^\s:@]+(?::latest)?(?=\s)/gim,
|
|
43
|
+
languages: ["dockerfile"],
|
|
44
|
+
fix: "Pin to a specific version tag: FROM node:20-alpine instead of FROM node:latest.",
|
|
45
|
+
fixCode: "# Pin to specific version\nFROM node:20-alpine\n# Not: FROM node:latest\n# Not: FROM node",
|
|
46
|
+
compliance: ["SOC2:CC7.1"],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "VG203",
|
|
50
|
+
name: "Secrets in ENV instruction",
|
|
51
|
+
severity: "critical",
|
|
52
|
+
owasp: "A01:2025 Broken Access Control",
|
|
53
|
+
description: "Secrets hardcoded in ENV instruction are visible in image history and to anyone with image access.",
|
|
54
|
+
pattern: /ENV\s+\S*(?:KEY|SECRET|PASSWORD|TOKEN|CREDENTIAL)\S*\s*=?\s*\S+/gi,
|
|
55
|
+
languages: ["dockerfile"],
|
|
56
|
+
fix: "Use runtime environment variables or Docker secrets instead of baking secrets into the image.",
|
|
57
|
+
fixCode: "# Don't bake secrets in image\n# Instead, pass at runtime:\n# docker run -e SECRET_KEY=xxx myapp\n# Or use Docker secrets / .env file",
|
|
58
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req2.3"],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "VG204",
|
|
62
|
+
name: "Using ADD instead of COPY",
|
|
63
|
+
severity: "medium",
|
|
64
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
65
|
+
description: "ADD has extra features (URL fetch, tar extraction) that can introduce unexpected behavior. Use COPY for local files.",
|
|
66
|
+
// Anchor to start of line + case-sensitive: Docker `ADD` instruction is uppercase and
|
|
67
|
+
// begins a line. Matching `add` case-insensitive caught `RUN pnpm add`, `apk add`,
|
|
68
|
+
// `yarn add`, etc. — package-manager subcommands inside RUN, not Docker instructions.
|
|
69
|
+
pattern: /^ADD\s+(?!https?:\/\/)\S+\s+\S+/gm,
|
|
70
|
+
languages: ["dockerfile"],
|
|
71
|
+
fix: "Use COPY instead of ADD for local files. Only use ADD for URLs or tar extraction.",
|
|
72
|
+
fixCode: "# Use COPY for local files\nCOPY ./src /app/src\n# Only use ADD for remote files or tar extraction\n# ADD https://example.com/file.tar.gz /app/",
|
|
73
|
+
compliance: ["SOC2:CC6.1"],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "VG205",
|
|
77
|
+
name: "Docker Socket Mount",
|
|
78
|
+
severity: "critical",
|
|
79
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
80
|
+
description: "Docker socket (/var/run/docker.sock) is mounted into a container. This grants the container full control over the Docker daemon — equivalent to root access on the host. CVE-2025-9074 and numerous container escape attacks exploit this.",
|
|
81
|
+
pattern: /(?:-v|--volume|volumes:)\s*.*\/var\/run\/docker\.sock/gi,
|
|
82
|
+
languages: ["yaml", "dockerfile", "shell"],
|
|
83
|
+
fix: "Never mount the Docker socket into application containers. Use Docker-in-Docker (dind) with TLS for CI runners that need Docker access.",
|
|
84
|
+
fixCode: '# BAD: full host Docker access\n# -v /var/run/docker.sock:/var/run/docker.sock\n\n# GOOD: use Docker-in-Docker with TLS for CI\nservices:\n dind:\n image: docker:dind\n privileged: true\n environment:\n DOCKER_TLS_CERTDIR: /certs',
|
|
85
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req6.5.10"],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "VG206",
|
|
89
|
+
name: "Dockerfile Missing HEALTHCHECK",
|
|
90
|
+
severity: "medium",
|
|
91
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
92
|
+
description: "Dockerfile has CMD or ENTRYPOINT but no HEALTHCHECK instruction. Without health checks, orchestrators cannot detect when the application is unresponsive, leading to silent failures and stale container routing.",
|
|
93
|
+
pattern: /^FROM\s+\S+[\s\S]*?(?:CMD|ENTRYPOINT)\s+(?:(?!HEALTHCHECK)[\s\S])*$/gim,
|
|
94
|
+
languages: ["dockerfile"],
|
|
95
|
+
fix: "Add a HEALTHCHECK instruction to verify the application is responding.",
|
|
96
|
+
fixCode: 'HEALTHCHECK --interval=30s --timeout=3s --retries=3 \\\n CMD curl -f http://localhost:3000/health || exit 1\n\nCMD ["node", "server.js"]',
|
|
97
|
+
compliance: ["SOC2:CC7.1"],
|
|
98
|
+
},
|
|
99
|
+
];
|