@ship-safe/cli 1.1.13 → 1.1.16
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/index.js +921 -36
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4125,6 +4125,59 @@ var SEVERITY_ORDER = {
|
|
|
4125
4125
|
low: 3,
|
|
4126
4126
|
info: 4
|
|
4127
4127
|
};
|
|
4128
|
+
var TIERS = {
|
|
4129
|
+
free: {
|
|
4130
|
+
id: "free",
|
|
4131
|
+
displayName: "Free",
|
|
4132
|
+
kind: "free",
|
|
4133
|
+
priceCents: 0,
|
|
4134
|
+
aiScans: 0,
|
|
4135
|
+
aiScansAreLifetime: false,
|
|
4136
|
+
fixPromptsPerMonth: 0
|
|
4137
|
+
},
|
|
4138
|
+
growth: {
|
|
4139
|
+
id: "growth",
|
|
4140
|
+
displayName: "Growth",
|
|
4141
|
+
kind: "subscription",
|
|
4142
|
+
priceCents: 1900,
|
|
4143
|
+
aiScans: 8,
|
|
4144
|
+
aiScansAreLifetime: false,
|
|
4145
|
+
fixPromptsPerMonth: 999
|
|
4146
|
+
},
|
|
4147
|
+
shield: {
|
|
4148
|
+
id: "shield",
|
|
4149
|
+
displayName: "Shield",
|
|
4150
|
+
kind: "subscription",
|
|
4151
|
+
priceCents: 3900,
|
|
4152
|
+
aiScans: 15,
|
|
4153
|
+
aiScansAreLifetime: false,
|
|
4154
|
+
fixPromptsPerMonth: 999
|
|
4155
|
+
},
|
|
4156
|
+
audit: {
|
|
4157
|
+
id: "audit",
|
|
4158
|
+
displayName: "Pro Audit",
|
|
4159
|
+
kind: "one_time",
|
|
4160
|
+
priceCents: 900,
|
|
4161
|
+
aiScans: 3,
|
|
4162
|
+
aiScansAreLifetime: true,
|
|
4163
|
+
fixPromptsPerMonth: 0
|
|
4164
|
+
}
|
|
4165
|
+
};
|
|
4166
|
+
var AI_SCAN_LIMITS = {
|
|
4167
|
+
free: TIERS.free.aiScans,
|
|
4168
|
+
growth: TIERS.growth.aiScans,
|
|
4169
|
+
shield: TIERS.shield.aiScans,
|
|
4170
|
+
audit: TIERS.audit.aiScans
|
|
4171
|
+
};
|
|
4172
|
+
var FIX_PROMPT_LIMITS = {
|
|
4173
|
+
free: TIERS.free.fixPromptsPerMonth,
|
|
4174
|
+
growth: TIERS.growth.fixPromptsPerMonth,
|
|
4175
|
+
shield: TIERS.shield.fixPromptsPerMonth,
|
|
4176
|
+
audit: TIERS.audit.fixPromptsPerMonth
|
|
4177
|
+
};
|
|
4178
|
+
var LIFETIME_TIERS = new Set(
|
|
4179
|
+
Object.values(TIERS).filter((t) => t.aiScansAreLifetime).map((t) => t.id)
|
|
4180
|
+
);
|
|
4128
4181
|
var LANGUAGE_EXTENSIONS = {
|
|
4129
4182
|
".js": "javascript",
|
|
4130
4183
|
".jsx": "javascript",
|
|
@@ -4145,7 +4198,25 @@ var LANGUAGE_EXTENSIONS = {
|
|
|
4145
4198
|
".yml": "yaml",
|
|
4146
4199
|
".yaml": "yaml",
|
|
4147
4200
|
".json": "json",
|
|
4148
|
-
".toml": "toml"
|
|
4201
|
+
".toml": "toml",
|
|
4202
|
+
// Added for AI-agent rules (round 7-vuln):
|
|
4203
|
+
".md": "markdown",
|
|
4204
|
+
".mdc": "markdown",
|
|
4205
|
+
// Cursor 2026 rules format
|
|
4206
|
+
".sql": "sql",
|
|
4207
|
+
".sh": "shell",
|
|
4208
|
+
".bash": "shell",
|
|
4209
|
+
".zsh": "shell"
|
|
4210
|
+
};
|
|
4211
|
+
var LANGUAGE_FILENAMES = {
|
|
4212
|
+
".cursorrules": "markdown",
|
|
4213
|
+
".windsurfrules": "markdown",
|
|
4214
|
+
".clinerules": "markdown",
|
|
4215
|
+
".continuerules": "markdown",
|
|
4216
|
+
".aiderules": "markdown",
|
|
4217
|
+
".roorules": "markdown",
|
|
4218
|
+
Dockerfile: "dockerfile",
|
|
4219
|
+
Containerfile: "dockerfile"
|
|
4149
4220
|
};
|
|
4150
4221
|
var IGNORE_PATTERNS = [
|
|
4151
4222
|
"node_modules",
|
|
@@ -4159,7 +4230,10 @@ var IGNORE_PATTERNS = [
|
|
|
4159
4230
|
".venv",
|
|
4160
4231
|
"vendor",
|
|
4161
4232
|
".idea",
|
|
4162
|
-
|
|
4233
|
+
// NOTE: `.vscode` intentionally removed — GitHub Copilot CVE-2025-53773
|
|
4234
|
+
// modifies `.vscode/settings.json` to bypass guardrails, and we need to
|
|
4235
|
+
// scan that file. Keep `.vscode/launch.json` etc out via specific rules
|
|
4236
|
+
// if false-positive volume becomes an issue.
|
|
4163
4237
|
"*.min.js",
|
|
4164
4238
|
"*.min.css",
|
|
4165
4239
|
"*.map",
|
|
@@ -4244,6 +4318,8 @@ var llmResponseSchema = external_exports.object({
|
|
|
4244
4318
|
import path from "path";
|
|
4245
4319
|
import { createHash } from "crypto";
|
|
4246
4320
|
function detectLanguage(filePath) {
|
|
4321
|
+
const basename = path.basename(filePath);
|
|
4322
|
+
if (LANGUAGE_FILENAMES[basename]) return LANGUAGE_FILENAMES[basename];
|
|
4247
4323
|
const ext = path.extname(filePath).toLowerCase();
|
|
4248
4324
|
return LANGUAGE_EXTENSIONS[ext] ?? "unknown";
|
|
4249
4325
|
}
|
|
@@ -4727,16 +4803,19 @@ var SECRET_RULES = [
|
|
|
4727
4803
|
{
|
|
4728
4804
|
id: "secrets/openai-api-key",
|
|
4729
4805
|
title: "OpenAI API Key Detected",
|
|
4730
|
-
description: "An OpenAI API key was found hardcoded.
|
|
4806
|
+
description: "An OpenAI API key was found hardcoded. Anyone with repo access (or anyone running your built bundle) can drain your balance \u2014 and bots scrape GitHub for these continuously, so assume the key is already compromised.",
|
|
4731
4807
|
severity: "critical",
|
|
4732
4808
|
confidence: "high",
|
|
4733
4809
|
cwe: "CWE-798",
|
|
4810
|
+
owasp: "A07:2021",
|
|
4734
4811
|
languages: ["*"],
|
|
4735
4812
|
patterns: [
|
|
4813
|
+
// Legacy 2023-era keys
|
|
4736
4814
|
{ regex: "sk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20}", type: "match" },
|
|
4737
|
-
|
|
4815
|
+
// Modern project-scoped, service-account, and sandbox key prefixes (2024+)
|
|
4816
|
+
{ regex: "sk-(?:proj|svcacct|None)-[a-zA-Z0-9_-]{20,}", type: "match" }
|
|
4738
4817
|
],
|
|
4739
|
-
fix: { description: "Move
|
|
4818
|
+
fix: { description: "Rotate at platform.openai.com immediately. Move the key to a server-only env var. If this was in a public repo for any window, assume it's drained.", suggestion: "process.env.OPENAI_API_KEY" }
|
|
4740
4819
|
},
|
|
4741
4820
|
{
|
|
4742
4821
|
id: "secrets/supabase-service-role",
|
|
@@ -4851,6 +4930,201 @@ var SECRET_RULES = [
|
|
|
4851
4930
|
],
|
|
4852
4931
|
excludePatterns: [{ regex: "(?:test|spec|mock|fixture|seed|placeholder|example|\\.test\\.|\\.spec\\.|__test__|jest|vitest)", type: "context_line" }],
|
|
4853
4932
|
fix: { description: "Remove default credentials from code. Use environment variables and generate strong, unique passwords for each deployment." }
|
|
4933
|
+
},
|
|
4934
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
4935
|
+
// Round 7-vuln additions: AI provider keys + vector DB keys + extension
|
|
4936
|
+
// publish tokens. Sourced from tasks/HANDOFF_CLAUDE_CODE.md §I + §4.3.
|
|
4937
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
4938
|
+
{
|
|
4939
|
+
id: "secrets/anthropic-api-key",
|
|
4940
|
+
title: "Anthropic API Key Detected",
|
|
4941
|
+
description: "An Anthropic API key (sk-ant-...) is sitting in source. Anyone with repo access can run up your Anthropic bill. Bots scrape GitHub for these continuously.",
|
|
4942
|
+
severity: "critical",
|
|
4943
|
+
confidence: "high",
|
|
4944
|
+
cwe: "CWE-798",
|
|
4945
|
+
owasp: "A07:2021",
|
|
4946
|
+
languages: ["*"],
|
|
4947
|
+
patterns: [{ regex: "sk-ant-[a-zA-Z0-9_-]{40,}", type: "match" }],
|
|
4948
|
+
excludePatterns: [{ regex: "(?:example|test|fake|dummy|placeholder|xxxx|YOUR_KEY)", type: "context_line" }],
|
|
4949
|
+
fix: { description: "Rotate at console.anthropic.com immediately. Move the key to a server-only env var. The key was already scraped \u2014 assume it's compromised, don't just remove the commit.", suggestion: "process.env.ANTHROPIC_API_KEY" }
|
|
4950
|
+
},
|
|
4951
|
+
{
|
|
4952
|
+
id: "secrets/google-ai-key",
|
|
4953
|
+
title: "Google AI Studio / Gemini API Key Detected",
|
|
4954
|
+
description: "A Google AI Studio API key (AIza...) used for Gemini API and AI Studio. Same scrape risk as other AI keys.",
|
|
4955
|
+
severity: "critical",
|
|
4956
|
+
confidence: "high",
|
|
4957
|
+
cwe: "CWE-798",
|
|
4958
|
+
owasp: "A07:2021",
|
|
4959
|
+
languages: ["*"],
|
|
4960
|
+
patterns: [
|
|
4961
|
+
{ regex: `(?:GEMINI|GOOGLE_AI|GOOGLE_GEN_AI|GENAI)_API_KEY\\s*[:=]\\s*["']?AIza[A-Za-z0-9_-]{35}`, type: "match" },
|
|
4962
|
+
{ regex: `["']AIza[A-Za-z0-9_-]{35}["']`, type: "match" }
|
|
4963
|
+
],
|
|
4964
|
+
excludePatterns: [{ regex: "(?:example|test|fake|placeholder)", type: "context_line" }],
|
|
4965
|
+
fix: { description: "Revoke in Google AI Studio (https://aistudio.google.com/apikey). Create a new key with API restrictions and move to server-side env vars.", suggestion: "process.env.GEMINI_API_KEY" }
|
|
4966
|
+
},
|
|
4967
|
+
{
|
|
4968
|
+
id: "secrets/grok-xai-key",
|
|
4969
|
+
title: "xAI / Grok API Key Detected",
|
|
4970
|
+
description: "An xAI/Grok API key (xai-...). Newer provider, same blast radius.",
|
|
4971
|
+
severity: "critical",
|
|
4972
|
+
confidence: "high",
|
|
4973
|
+
cwe: "CWE-798",
|
|
4974
|
+
languages: ["*"],
|
|
4975
|
+
patterns: [{ regex: "\\bxai-[a-zA-Z0-9]{60,}\\b", type: "match" }],
|
|
4976
|
+
fix: { description: "Rotate at https://console.x.ai. Server-side only from now on.", suggestion: "process.env.XAI_API_KEY" }
|
|
4977
|
+
},
|
|
4978
|
+
{
|
|
4979
|
+
id: "secrets/huggingface-token",
|
|
4980
|
+
title: "Hugging Face Token Detected",
|
|
4981
|
+
description: "A Hugging Face access token (hf_...). Used for model downloads, inference API, dataset pushes. Has push permissions on repos by default.",
|
|
4982
|
+
severity: "high",
|
|
4983
|
+
confidence: "high",
|
|
4984
|
+
cwe: "CWE-798",
|
|
4985
|
+
languages: ["*"],
|
|
4986
|
+
patterns: [{ regex: "\\bhf_[A-Za-z0-9]{34}\\b", type: "match" }],
|
|
4987
|
+
fix: { description: "Revoke at huggingface.co/settings/tokens. Create a read-only token if that's all you need.", suggestion: "process.env.HUGGINGFACE_TOKEN" }
|
|
4988
|
+
},
|
|
4989
|
+
{
|
|
4990
|
+
id: "secrets/replicate-token",
|
|
4991
|
+
title: "Replicate API Token Detected",
|
|
4992
|
+
description: "A Replicate API token (r8_...). Replicate runs ML models \u2014 your billing balance is directly burnable.",
|
|
4993
|
+
severity: "critical",
|
|
4994
|
+
confidence: "high",
|
|
4995
|
+
cwe: "CWE-798",
|
|
4996
|
+
languages: ["*"],
|
|
4997
|
+
patterns: [{ regex: "\\br8_[A-Za-z0-9]{37,40}\\b", type: "match" }],
|
|
4998
|
+
fix: { description: "Rotate at replicate.com/account/api-tokens immediately.", suggestion: "process.env.REPLICATE_API_TOKEN" }
|
|
4999
|
+
},
|
|
5000
|
+
{
|
|
5001
|
+
id: "secrets/groq-api-key",
|
|
5002
|
+
title: "Groq API Key Detected",
|
|
5003
|
+
description: "A Groq API key (gsk_...). Same blast radius as OpenAI keys \u2014 burnable balance, no scope.",
|
|
5004
|
+
severity: "critical",
|
|
5005
|
+
confidence: "high",
|
|
5006
|
+
cwe: "CWE-798",
|
|
5007
|
+
languages: ["*"],
|
|
5008
|
+
patterns: [{ regex: "\\bgsk_[A-Za-z0-9]{52,56}\\b", type: "match" }],
|
|
5009
|
+
fix: { description: "Rotate at console.groq.com/keys.", suggestion: "process.env.GROQ_API_KEY" }
|
|
5010
|
+
},
|
|
5011
|
+
{
|
|
5012
|
+
id: "secrets/perplexity-api-key",
|
|
5013
|
+
title: "Perplexity API Key Detected",
|
|
5014
|
+
description: "A Perplexity API key (pplx-...).",
|
|
5015
|
+
severity: "high",
|
|
5016
|
+
confidence: "high",
|
|
5017
|
+
cwe: "CWE-798",
|
|
5018
|
+
languages: ["*"],
|
|
5019
|
+
patterns: [{ regex: "\\bpplx-[A-Za-z0-9]{48,56}\\b", type: "match" }],
|
|
5020
|
+
fix: { description: "Rotate at perplexity.ai/settings/api.", suggestion: "process.env.PERPLEXITY_API_KEY" }
|
|
5021
|
+
},
|
|
5022
|
+
{
|
|
5023
|
+
id: "secrets/together-api-key",
|
|
5024
|
+
title: "Together AI API Key Detected",
|
|
5025
|
+
description: "A Together AI API key. 64-hex-char format \u2014 requires the env-var context to fire to avoid false positives on file hashes.",
|
|
5026
|
+
severity: "high",
|
|
5027
|
+
confidence: "medium",
|
|
5028
|
+
cwe: "CWE-798",
|
|
5029
|
+
languages: ["*"],
|
|
5030
|
+
patterns: [{ regex: `(?:TOGETHER_API_KEY|TOGETHERAI_API_KEY)\\s*[:=]\\s*["']?[a-f0-9]{64}["']?`, type: "match" }],
|
|
5031
|
+
fix: { description: "Rotate at api.together.xyz/settings/api-keys.", suggestion: "process.env.TOGETHER_API_KEY" }
|
|
5032
|
+
},
|
|
5033
|
+
{
|
|
5034
|
+
id: "secrets/elevenlabs-api-key",
|
|
5035
|
+
title: "ElevenLabs API Key Detected",
|
|
5036
|
+
description: "An ElevenLabs API key. Voice synthesis charges run fast \u2014 a prompt-injection that generates 30-minute outputs against a leaked key burns the balance in hours.",
|
|
5037
|
+
severity: "high",
|
|
5038
|
+
confidence: "medium",
|
|
5039
|
+
cwe: "CWE-798",
|
|
5040
|
+
languages: ["*"],
|
|
5041
|
+
// Both patterns must match the file (engine §1.1b context_line filter)
|
|
5042
|
+
// because 32-hex strings appear all over (hashes, UUIDs).
|
|
5043
|
+
patterns: [
|
|
5044
|
+
{ regex: `(?:ELEVENLABS|XI)_API_KEY\\s*[:=]\\s*["']?(?:sk_)?[a-f0-9]{32}`, type: "match" }
|
|
5045
|
+
],
|
|
5046
|
+
fix: { description: "Rotate at elevenlabs.io/app/settings/api-keys.", suggestion: "process.env.ELEVENLABS_API_KEY" }
|
|
5047
|
+
},
|
|
5048
|
+
{
|
|
5049
|
+
id: "secrets/mistral-api-key",
|
|
5050
|
+
title: "Mistral AI API Key Detected",
|
|
5051
|
+
description: "A Mistral AI API key.",
|
|
5052
|
+
severity: "high",
|
|
5053
|
+
confidence: "medium",
|
|
5054
|
+
cwe: "CWE-798",
|
|
5055
|
+
languages: ["*"],
|
|
5056
|
+
patterns: [{ regex: `\\bMISTRAL_API_KEY\\s*[:=]\\s*["']?[A-Za-z0-9]{32,}["']?`, type: "match" }],
|
|
5057
|
+
fix: { description: "Rotate at console.mistral.ai/api-keys.", suggestion: "process.env.MISTRAL_API_KEY" }
|
|
5058
|
+
},
|
|
5059
|
+
{
|
|
5060
|
+
id: "secrets/cohere-api-key",
|
|
5061
|
+
title: "Cohere API Key Detected",
|
|
5062
|
+
description: "A Cohere API key.",
|
|
5063
|
+
severity: "high",
|
|
5064
|
+
confidence: "medium",
|
|
5065
|
+
cwe: "CWE-798",
|
|
5066
|
+
languages: ["*"],
|
|
5067
|
+
patterns: [{ regex: `\\bCOHERE_API_KEY\\s*[:=]\\s*["']?[A-Za-z0-9]{40,}["']?`, type: "match" }],
|
|
5068
|
+
fix: { description: "Rotate at dashboard.cohere.com/api-keys.", suggestion: "process.env.COHERE_API_KEY" }
|
|
5069
|
+
},
|
|
5070
|
+
{
|
|
5071
|
+
id: "secrets/deepseek-api-key",
|
|
5072
|
+
title: "DeepSeek API Key Detected",
|
|
5073
|
+
description: "A DeepSeek API key (sk-...). Format collides with OpenAI legacy keys, so detection requires the DEEPSEEK_API_KEY env-var context to avoid false positives.",
|
|
5074
|
+
severity: "high",
|
|
5075
|
+
confidence: "high",
|
|
5076
|
+
cwe: "CWE-798",
|
|
5077
|
+
languages: ["*"],
|
|
5078
|
+
patterns: [
|
|
5079
|
+
{ regex: `(?:DEEPSEEK|DEEP_SEEK)_API_KEY\\s*[:=]\\s*["']?sk-[a-f0-9]{32}["']?`, type: "match" }
|
|
5080
|
+
],
|
|
5081
|
+
fix: { description: "Rotate at platform.deepseek.com/api_keys.", suggestion: "process.env.DEEPSEEK_API_KEY" }
|
|
5082
|
+
},
|
|
5083
|
+
{
|
|
5084
|
+
id: "secrets/pinecone-api-key",
|
|
5085
|
+
title: "Pinecone API Key Detected",
|
|
5086
|
+
description: "A Pinecone vector DB API key. Pinecone keys grant read+write on every index by default \u2014 an attacker with the key can dump or wipe your embeddings.",
|
|
5087
|
+
severity: "critical",
|
|
5088
|
+
confidence: "high",
|
|
5089
|
+
cwe: "CWE-798",
|
|
5090
|
+
languages: ["*"],
|
|
5091
|
+
patterns: [
|
|
5092
|
+
{ regex: "\\bpcsk_[A-Za-z0-9_]{40,}\\b", type: "match" }
|
|
5093
|
+
],
|
|
5094
|
+
fix: { description: "Rotate at app.pinecone.io. Restrict the new key to a specific project and consider creating index-scoped keys.", suggestion: "process.env.PINECONE_API_KEY" }
|
|
5095
|
+
},
|
|
5096
|
+
{
|
|
5097
|
+
id: "secrets/weaviate-api-key",
|
|
5098
|
+
title: "Weaviate API Key Detected",
|
|
5099
|
+
description: "A Weaviate API key.",
|
|
5100
|
+
severity: "high",
|
|
5101
|
+
confidence: "medium",
|
|
5102
|
+
cwe: "CWE-798",
|
|
5103
|
+
languages: ["*"],
|
|
5104
|
+
patterns: [{ regex: `(?:WEAVIATE_API_KEY|WEAVIATE_ADMIN_API_KEY)\\s*[:=]\\s*["']?[A-Za-z0-9-]{32,}["']?`, type: "match" }],
|
|
5105
|
+
fix: { description: "Rotate the key in your Weaviate Cloud console. Prefer read-only keys for client-reachable code.", suggestion: "process.env.WEAVIATE_API_KEY" }
|
|
5106
|
+
},
|
|
5107
|
+
{
|
|
5108
|
+
id: "secrets/vsce-publish-token",
|
|
5109
|
+
title: "VS Code Marketplace Publish Token Detected",
|
|
5110
|
+
description: "A VSCE personal access token publishes extensions to the VS Code Marketplace. The Clinejection attack stole this exact token via CI cache poisoning and published malware to millions of installs. Rotate immediately.",
|
|
5111
|
+
severity: "critical",
|
|
5112
|
+
confidence: "high",
|
|
5113
|
+
cwe: "CWE-798",
|
|
5114
|
+
languages: ["*"],
|
|
5115
|
+
patterns: [{ regex: `(?:VSCE_PAT|VSCE_TOKEN|VSCODE_MARKETPLACE_TOKEN)\\s*[:=]\\s*["']?[A-Za-z0-9]{52,}`, type: "match" }],
|
|
5116
|
+
fix: { description: "Rotate the token in https://dev.azure.com immediately. Move to GitHub Actions OIDC for publishing if possible. Audit the extension's recent versions for tampering.", suggestion: "process.env.VSCE_PAT" }
|
|
5117
|
+
},
|
|
5118
|
+
{
|
|
5119
|
+
id: "secrets/ovsx-publish-token",
|
|
5120
|
+
title: "Open VSX Publish Token Detected",
|
|
5121
|
+
description: "An Open VSX publish token. Same blast radius as VSCE_PAT \u2014 the Clinejection attack stole this token alongside.",
|
|
5122
|
+
severity: "critical",
|
|
5123
|
+
confidence: "high",
|
|
5124
|
+
cwe: "CWE-798",
|
|
5125
|
+
languages: ["*"],
|
|
5126
|
+
patterns: [{ regex: `(?:OVSX_PAT|OVSX_TOKEN|OPEN_VSX_TOKEN)\\s*[:=]\\s*["']?[A-Za-z0-9-]{30,}`, type: "match" }],
|
|
5127
|
+
fix: { description: "Rotate at https://open-vsx.org/user-settings/tokens. Re-publish the latest known-good version of your extension after confirming the source hasn't been tampered with.", suggestion: "process.env.OVSX_PAT" }
|
|
4854
5128
|
}
|
|
4855
5129
|
];
|
|
4856
5130
|
var INJECTION_RULES = [
|
|
@@ -5091,6 +5365,25 @@ var CONFIG_RULES = [
|
|
|
5091
5365
|
patterns: [{ regex: "(?:NextAuth|authOptions|nextauth)\\s*[:=(]", type: "match" }],
|
|
5092
5366
|
excludePatterns: [{ regex: "(?:NEXTAUTH_SECRET|secret\\s*:\\s*process\\.env|AUTH_SECRET|NEXT_AUTH_SECRET)", type: "context_line" }],
|
|
5093
5367
|
fix: { description: "Set the NEXTAUTH_SECRET environment variable in production. Generate a strong random secret with openssl rand -base64 32.", suggestion: "secret: process.env.NEXTAUTH_SECRET" }
|
|
5368
|
+
},
|
|
5369
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
5370
|
+
// Round 7-vuln: agent-workspace config files committed to repo.
|
|
5371
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
5372
|
+
{
|
|
5373
|
+
id: "config/agent-config-tracked",
|
|
5374
|
+
title: "Agent Workspace Config Committed to Repo",
|
|
5375
|
+
description: "A file like .cursor/mcp.json, .claude/settings.local.json, or .windsurf/config.json is in the repo. These are per-machine workspace files that often contain MCP server credentials, API keys, and absolute paths that leak the contributor's home directory. They should never be tracked.",
|
|
5376
|
+
severity: "medium",
|
|
5377
|
+
confidence: "medium",
|
|
5378
|
+
cwe: "CWE-538",
|
|
5379
|
+
languages: ["json", "yaml"],
|
|
5380
|
+
patterns: [
|
|
5381
|
+
// Fires only on the workspace-config filenames themselves.
|
|
5382
|
+
{ regex: "(?:^|/)(?:\\.cursor/mcp\\.json|\\.claude/settings\\.local\\.json|\\.windsurf/config\\.json|\\.cline/settings\\.json|\\.aider\\.conf\\.yml)$", type: "file_path" },
|
|
5383
|
+
// Marker: any non-empty content. The presence of the file IS the finding.
|
|
5384
|
+
{ regex: "\\S", type: "match" }
|
|
5385
|
+
],
|
|
5386
|
+
fix: { description: "Add to .gitignore, then `git rm --cached <file>` to untrack. Move secrets in the file to environment variables. Use a `.cursor/mcp.shared.json` (or similar tool convention) for team-shared, secret-free settings." }
|
|
5094
5387
|
}
|
|
5095
5388
|
];
|
|
5096
5389
|
var PII_RULES = [
|
|
@@ -5353,6 +5646,76 @@ var BAAS_RULES = [
|
|
|
5353
5646
|
],
|
|
5354
5647
|
excludePatterns: [{ regex: `(?:allowedRedirects|validRedirects|safeUrls|startsWith\\s*\\(\\s*["']https://)`, type: "context_line" }],
|
|
5355
5648
|
fix: { description: "Use a hardcoded or whitelisted redirect URL for OAuth callbacks. Never use raw user input.", suggestion: "redirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`" }
|
|
5649
|
+
},
|
|
5650
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
5651
|
+
// Round 7-vuln additions: post-Lovable-CVE Supabase RLS patterns. The
|
|
5652
|
+
// existing `supabase-rls-disabled` rule only flags MISSING RLS. These flag
|
|
5653
|
+
// RLS that LOOKS protective but isn't — `USING (true)`, over-granted anon
|
|
5654
|
+
// role, service-role used in client-reachable code, JWT-claim trust.
|
|
5655
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
5656
|
+
{
|
|
5657
|
+
id: "baas/supabase-rls-policy-allows-all",
|
|
5658
|
+
title: "Supabase RLS Policy Allows All Rows",
|
|
5659
|
+
description: "This row-level-security policy uses USING (true), USING (1=1), or WITH CHECK (true). RLS is technically enabled \u2014 so the table looks protected and security scanners that only check `enable row level security` pass \u2014 but the policy itself authorizes everything. Lovable's 'security scan' missed exactly this pattern. CVE-2025-48757 fallout: 170 apps shipped this.",
|
|
5660
|
+
severity: "critical",
|
|
5661
|
+
confidence: "high",
|
|
5662
|
+
cwe: "CWE-285",
|
|
5663
|
+
owasp: "A01:2021",
|
|
5664
|
+
languages: ["sql"],
|
|
5665
|
+
patterns: [
|
|
5666
|
+
{ regex: "create\\s+policy[^;]+(?:using|with\\s+check)\\s*\\(\\s*(?:true|1\\s*=\\s*1)\\s*\\)", type: "match" },
|
|
5667
|
+
{ regex: "create\\s+policy[^;]+using\\s*\\(\\s*auth\\.role\\(\\)\\s*=\\s*'authenticated'\\s*\\)", type: "match" }
|
|
5668
|
+
],
|
|
5669
|
+
fix: { description: "Replace `USING (true)` with a real predicate \u2014 typically `USING (auth.uid() = user_id)`. If the table genuinely allows public read, scope it by operation (`FOR SELECT`) and add a separate restrictive policy for writes." }
|
|
5670
|
+
},
|
|
5671
|
+
{
|
|
5672
|
+
id: "baas/supabase-anon-overgrant",
|
|
5673
|
+
title: "Postgres Anon Role Granted Broad Privileges",
|
|
5674
|
+
description: "GRANT ALL, GRANT INSERT, or GRANT DELETE to the `anon` role gives unauthenticated users database-level write access. Even with RLS, this widens the attack surface \u2014 `anon` should only have SELECT on tables you intend to expose, and EXECUTE on RPC functions you've audited.",
|
|
5675
|
+
severity: "high",
|
|
5676
|
+
confidence: "high",
|
|
5677
|
+
cwe: "CWE-732",
|
|
5678
|
+
owasp: "A01:2021",
|
|
5679
|
+
languages: ["sql"],
|
|
5680
|
+
patterns: [
|
|
5681
|
+
{ regex: "grant\\s+(?:all|insert|update|delete|truncate)\\s+(?:on[^;]+)?to\\s+anon", type: "match" }
|
|
5682
|
+
],
|
|
5683
|
+
fix: { description: "Revoke broad grants. `REVOKE ALL ON <table> FROM anon;` then `GRANT SELECT ON <table> TO anon;` only for tables that should be publicly readable." }
|
|
5684
|
+
},
|
|
5685
|
+
{
|
|
5686
|
+
id: "baas/supabase-service-role-client-reachable",
|
|
5687
|
+
title: "Service-Role Key Used in Client-Reachable Route",
|
|
5688
|
+
description: "This route handler uses the Supabase service-role client but lives in a path the client can call (no auth gate, no internal/server-only marker). Service role bypasses RLS entirely \u2014 a single unauthenticated request reads every table. The April 2026 Lovable mass breach included this pattern across pre-Nov-2025 projects.",
|
|
5689
|
+
severity: "critical",
|
|
5690
|
+
confidence: "medium",
|
|
5691
|
+
cwe: "CWE-269",
|
|
5692
|
+
owasp: "A01:2021",
|
|
5693
|
+
languages: ["javascript", "typescript"],
|
|
5694
|
+
patterns: [
|
|
5695
|
+
{ regex: "createClient\\s*\\([^,]+,\\s*process\\.env\\.(?:SUPABASE_)?SERVICE_ROLE_KEY", type: "match" }
|
|
5696
|
+
],
|
|
5697
|
+
excludePatterns: [
|
|
5698
|
+
{ regex: "(?:\\.server\\.|/api/internal|cron|webhook|middleware\\.ts|requireAuth|auth\\.uid|getServerSession)", type: "context_line" },
|
|
5699
|
+
{ regex: "(?:\\.server\\.|/api/internal|cron|webhook|middleware\\.ts)", type: "file_path" }
|
|
5700
|
+
],
|
|
5701
|
+
fix: { description: "Either move this logic behind explicit auth (e.g. requireUser() at the top of the handler), or replace the service-role client with the anon client + RLS. Never use service role in an endpoint a logged-out browser can reach." }
|
|
5702
|
+
},
|
|
5703
|
+
{
|
|
5704
|
+
id: "baas/supabase-rls-policy-jwt-claim-not-verified",
|
|
5705
|
+
title: "Supabase RLS Policy Trusts a JWT Claim Without Issuing It",
|
|
5706
|
+
description: "This policy authorizes based on auth.jwt() ->> 'role' or auth.jwt() ->> 'org_id' from a claim Supabase doesn't issue by default. If the claim comes from a third-party JWT (Clerk, Auth0, Firebase) and you haven't configured Supabase to validate the issuer, an attacker can craft their own JWT and bypass the policy.",
|
|
5707
|
+
severity: "high",
|
|
5708
|
+
confidence: "low",
|
|
5709
|
+
cwe: "CWE-345",
|
|
5710
|
+
owasp: "A01:2021",
|
|
5711
|
+
languages: ["sql"],
|
|
5712
|
+
patterns: [
|
|
5713
|
+
{ regex: "auth\\.jwt\\(\\)\\s*->>?\\s*'(?:role|org_id|tenant|company|workspace_id|account_id)'", type: "match" }
|
|
5714
|
+
],
|
|
5715
|
+
excludePatterns: [
|
|
5716
|
+
{ regex: "(?:auth\\.uid|user_id|owner_id)", type: "context_line" }
|
|
5717
|
+
],
|
|
5718
|
+
fix: { description: "Either issue these claims via Supabase Auth Hooks (so Supabase is the JWT issuer), or configure third-party JWT verification with a strict issuer + audience check. Don't read claims you don't issue." }
|
|
5356
5719
|
}
|
|
5357
5720
|
];
|
|
5358
5721
|
var LLM_RULES = [
|
|
@@ -5489,6 +5852,158 @@ var LLM_RULES = [
|
|
|
5489
5852
|
{ regex: "(?:query|execute|sql|raw)\\s*\\(\\s*(?:completion|response|result|output|llm|ai|gpt|claude)[.\\[]", type: "match" }
|
|
5490
5853
|
],
|
|
5491
5854
|
fix: { description: "Never execute LLM output as code. Use structured output parsing with validation, or run generated code in a sandboxed environment." }
|
|
5855
|
+
},
|
|
5856
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
5857
|
+
// Round 7-vuln additions: MCP tool pinning, LangChain SSRF/path traversal,
|
|
5858
|
+
// Vercel AI SDK input handling, cost-exhaustion (max_tokens / rate limit),
|
|
5859
|
+
// .env-into-prompt, Codex branch-shell, OpenAI Assistants allowlist,
|
|
5860
|
+
// git-hook writes, agent-on-PR-content.
|
|
5861
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
5862
|
+
{
|
|
5863
|
+
id: "llm/mcp-tool-no-pinning",
|
|
5864
|
+
title: "MCP Tool Used Without Version Pinning",
|
|
5865
|
+
description: "This code calls an MCP tool by name without checking the server's identity hash or pinning the tool description. A 'rug pull' (CVE-2025-54136) \u2014 server updates its tool description after you approved it \u2014 silently changes what the tool does. Your code keeps calling `add` and ends up exfiltrating data.",
|
|
5866
|
+
severity: "medium",
|
|
5867
|
+
confidence: "low",
|
|
5868
|
+
cwe: "CWE-345",
|
|
5869
|
+
languages: ["javascript", "typescript", "python"],
|
|
5870
|
+
patterns: [
|
|
5871
|
+
{ regex: `(?:mcpClient|mcp_client|MCPClient)\\.(?:callTool|invoke|call)\\s*\\(\\s*["']`, type: "match" }
|
|
5872
|
+
],
|
|
5873
|
+
excludePatterns: [{ regex: "(?:hash|fingerprint|version|pinned|approved)", type: "context_line" }],
|
|
5874
|
+
fix: { description: "Record a hash of the tool description the first time you approve a server. Re-verify on every call. Reject calls whose description changed since approval." }
|
|
5875
|
+
},
|
|
5876
|
+
{
|
|
5877
|
+
id: "llm/langchain-recursive-url-loader-unsafe",
|
|
5878
|
+
title: "LangChain RecursiveUrlLoader Used Without SSRF Guard",
|
|
5879
|
+
description: "RecursiveUrlLoader follows HTTP redirects in a way that bypasses URL validators \u2014 CVE-2026-27795 lets an attacker hand you a benign-looking URL that 302s to cloud metadata. If you load attacker-supplied URLs into a RAG pipeline, you leak 169.254.169.254 and friends.",
|
|
5880
|
+
severity: "high",
|
|
5881
|
+
confidence: "medium",
|
|
5882
|
+
cwe: "CWE-918",
|
|
5883
|
+
owasp: "A10:2021",
|
|
5884
|
+
languages: ["python", "javascript", "typescript"],
|
|
5885
|
+
patterns: [
|
|
5886
|
+
{ regex: "(?:RecursiveUrlLoader|recursive_url_loader)\\s*\\(", type: "match" }
|
|
5887
|
+
],
|
|
5888
|
+
excludePatterns: [{ regex: "(?:allowlist|allow_list|allowed_domains|domain_filter|ssrf_guard)", type: "context_line" }],
|
|
5889
|
+
fix: { description: "Upgrade @langchain/community to 1.1.14+ (Node) or langchain-community to the patched release (Python). Wrap loader calls in an SSRF-safe HTTP client that blocks RFC 1918, link-local, and cloud-metadata ranges before AND after redirects." }
|
|
5890
|
+
},
|
|
5891
|
+
{
|
|
5892
|
+
id: "llm/langchain-load-prompt-from-path",
|
|
5893
|
+
title: "LangChain Loads Prompt Template from Untrusted Path",
|
|
5894
|
+
description: "load_prompt(...) (CVE-2026-34070) traverses any path you give it. If the path comes from request input, an attacker reads arbitrary files \u2014 /etc/passwd, .env, SSH keys.",
|
|
5895
|
+
severity: "high",
|
|
5896
|
+
confidence: "medium",
|
|
5897
|
+
cwe: "CWE-22",
|
|
5898
|
+
owasp: "A01:2021",
|
|
5899
|
+
languages: ["python", "javascript", "typescript"],
|
|
5900
|
+
patterns: [
|
|
5901
|
+
{ regex: "(?:load_prompt|loadPrompt)\\s*\\([^)]*(?:req\\.|request\\.|input|user|body|query|param)", type: "match" }
|
|
5902
|
+
],
|
|
5903
|
+
fix: { description: "Never pass untrusted strings to load_prompt. Maintain an explicit registry of allowed prompt names and look up the path server-side." }
|
|
5904
|
+
},
|
|
5905
|
+
{
|
|
5906
|
+
id: "llm/ai-sdk-input-as-prompt",
|
|
5907
|
+
title: "Vercel AI SDK generateText/streamText Called with Raw User Input",
|
|
5908
|
+
description: "generateText({ prompt: userInput }) and streamText({ messages: [...] }) with raw user input is the canonical Vercel AI SDK prompt-injection point. Users can override system instructions, extract the system prompt, or trigger arbitrary tool calls.",
|
|
5909
|
+
severity: "high",
|
|
5910
|
+
confidence: "medium",
|
|
5911
|
+
cwe: "CWE-77",
|
|
5912
|
+
languages: ["javascript", "typescript"],
|
|
5913
|
+
patterns: [
|
|
5914
|
+
{ regex: "(?:generateText|streamText|generateObject|streamObject)\\s*\\(\\s*\\{[^}]*prompt\\s*:\\s*(?:req\\.|request\\.|body\\.|input|userInput|message)", type: "match" }
|
|
5915
|
+
],
|
|
5916
|
+
excludePatterns: [{ regex: "(?:sanitize|validate|zod|schema|filter)", type: "context_line" }],
|
|
5917
|
+
fix: { description: "Always pass user input as a separate `user` message, never as the prompt string and never interpolated into the system message. Validate with Zod before sending.", suggestion: "streamText({ system: SYSTEM_PROMPT, messages: [{ role: 'user', content: sanitize(input) }] })" }
|
|
5918
|
+
},
|
|
5919
|
+
{
|
|
5920
|
+
id: "llm/agent-runs-on-unsanitized-pr-content",
|
|
5921
|
+
title: "AI Agent Run on PR Title/Body Without Sanitizing",
|
|
5922
|
+
description: "This code passes PR titles, bodies, or comments straight into an agent prompt. The 'Comment and Control' attack chain has confirmed exfil of ANTHROPIC_API_KEY and GITHUB_TOKEN via PR titles in Claude Code, fake 'Trusted Content Section' blocks in Gemini CLI, and HTML comments in Copilot Agent.",
|
|
5923
|
+
severity: "critical",
|
|
5924
|
+
confidence: "medium",
|
|
5925
|
+
cwe: "CWE-77",
|
|
5926
|
+
owasp: "A03:2021",
|
|
5927
|
+
languages: ["javascript", "typescript", "python"],
|
|
5928
|
+
patterns: [
|
|
5929
|
+
{ regex: "(?:pull_request|pullRequest|pr|issue)\\.(?:title|body|head_ref).{0,200}(?:prompt|messages|systemPrompt|userMessage)", type: "match" },
|
|
5930
|
+
{ regex: "(?:prompt|content)\\s*[:=]\\s*`[^`]*\\$\\{(?:pr|pullRequest|issue|comment)\\.(?:title|body)", type: "match" }
|
|
5931
|
+
],
|
|
5932
|
+
fix: { description: "Treat PR content as untrusted data, never as instructions. Strip prompt-injection markers (<system>, </system>, 'ignore previous'). Move the actual instructions to a server-side system prompt and pass PR content as a quoted block inside a user message." }
|
|
5933
|
+
},
|
|
5934
|
+
{
|
|
5935
|
+
id: "llm/no-max-tokens",
|
|
5936
|
+
title: "LLM Call Without max_tokens or Cost Limit",
|
|
5937
|
+
description: "This LLM call has no max_tokens (OpenAI/Anthropic), max_output_tokens (Gemini), or equivalent ceiling. A prompt-injection or a single buggy loop can rack up thousands of dollars on a free-tier app. Uber exhausted its 2026 AI budget months into the year \u2014 this is the pattern.",
|
|
5938
|
+
severity: "medium",
|
|
5939
|
+
confidence: "low",
|
|
5940
|
+
cwe: "CWE-770",
|
|
5941
|
+
owasp: "A04:2021",
|
|
5942
|
+
languages: ["javascript", "typescript", "python"],
|
|
5943
|
+
patterns: [
|
|
5944
|
+
{ regex: "(?:openai|anthropic|gemini|generativeai)\\.[a-z_]+\\.(?:create|generate|complete|messages)\\s*\\(\\s*\\{[^}]*\\bmodel\\s*:", type: "match" }
|
|
5945
|
+
],
|
|
5946
|
+
excludePatterns: [{ regex: "(?:max_tokens|max_output_tokens|maxTokens|maxOutputTokens|max_completion_tokens)", type: "context_line" }],
|
|
5947
|
+
fix: { description: "Always set a max_tokens ceiling. Add a per-user / per-IP rate limiter at the API edge. Track cost per request via the response usage object and refuse new requests when the user passes a daily threshold.", suggestion: "const completion = await openai.chat.completions.create({ model: 'gpt-4o', messages, max_tokens: 1024 });" }
|
|
5948
|
+
},
|
|
5949
|
+
{
|
|
5950
|
+
id: "llm/env-file-read-into-prompt",
|
|
5951
|
+
title: ".env or Secrets File Read into LLM Prompt",
|
|
5952
|
+
description: "This code reads a secrets file (.env, .aws/credentials, ~/.ssh/) and passes it into an LLM prompt or message. The LLM provider now has your secrets in their logs; a prompt injection in the response can exfiltrate them; and any output filtering you do is best-effort.",
|
|
5953
|
+
severity: "critical",
|
|
5954
|
+
confidence: "medium",
|
|
5955
|
+
cwe: "CWE-200",
|
|
5956
|
+
owasp: "A01:2021",
|
|
5957
|
+
languages: ["javascript", "typescript", "python"],
|
|
5958
|
+
patterns: [
|
|
5959
|
+
{ regex: "(?:readFile|readFileSync|fs\\.read|open|Path\\([^)]+\\)\\.read)[\\s\\S]{0,80}(?:\\.env|credentials|\\.ssh|secret|\\.pem|id_rsa)[\\s\\S]{0,300}(?:prompt|messages|content|completion|generateText|streamText)", type: "match" }
|
|
5960
|
+
],
|
|
5961
|
+
fix: { description: "Never put secrets in a prompt. If the agent needs to act on credentials, give it a tool that uses them internally and never returns or echoes them. Redact env files before passing any config snippet to an LLM." }
|
|
5962
|
+
},
|
|
5963
|
+
{
|
|
5964
|
+
id: "llm/agent-writes-to-git-hooks",
|
|
5965
|
+
title: "Code Writes to .git/hooks (CVE-2026-26268 vector)",
|
|
5966
|
+
description: "This code writes to .git/hooks/. Cursor IDE CVE-2026-26268 used exactly this \u2014 an agent cloning a repo into a workspace triggers pre-commit hooks attackers placed there. If your own code writes hooks based on agent output or external input, you've recreated the bug.",
|
|
5967
|
+
severity: "high",
|
|
5968
|
+
confidence: "high",
|
|
5969
|
+
cwe: "CWE-94",
|
|
5970
|
+
owasp: "A03:2021",
|
|
5971
|
+
languages: ["javascript", "typescript", "python", "shell"],
|
|
5972
|
+
patterns: [
|
|
5973
|
+
{ regex: `(?:writeFile|writeFileSync|open|Path\\([^)]+\\)\\.write)[\\s\\S]{0,80}["']\\.git/hooks/`, type: "match" },
|
|
5974
|
+
{ regex: "chmod\\s+\\+x[\\s\\S]{0,80}\\.git/hooks/", type: "match" }
|
|
5975
|
+
],
|
|
5976
|
+
fix: { description: "Don't write Git hooks from application code. If you need pre-commit checks, install them via husky or pre-commit at setup time with the user's explicit consent." }
|
|
5977
|
+
},
|
|
5978
|
+
{
|
|
5979
|
+
id: "llm/codex-branch-name-shell-injection",
|
|
5980
|
+
title: "Branch Name or Repo Field Passed to Shell Without Sanitizing",
|
|
5981
|
+
description: "OpenAI Codex CVE (reported Dec 2025, patched Feb 2026) shipped because branch names were passed unsanitized into shell commands inside the agent container, letting any user with branch-create permission run arbitrary commands and exfiltrate the GitHub token. Same shape applies to any agent that runs shell with attacker-controllable repo metadata.",
|
|
5982
|
+
severity: "critical",
|
|
5983
|
+
confidence: "medium",
|
|
5984
|
+
cwe: "CWE-78",
|
|
5985
|
+
owasp: "A03:2021",
|
|
5986
|
+
languages: ["javascript", "typescript", "python"],
|
|
5987
|
+
patterns: [
|
|
5988
|
+
{ regex: "(?:exec|spawn|execSync|child_process|subprocess\\.(?:run|call|Popen))[\\s\\S]{0,100}(?:branch|ref|head_ref|base_ref|repo_name|pr_title)", type: "match" },
|
|
5989
|
+
{ regex: "`[^`]*\\$\\{(?:branch|ref|head|repo)Name", type: "match" }
|
|
5990
|
+
],
|
|
5991
|
+
excludePatterns: [{ regex: "(?:shellQuote|escapeShellArg|shlex\\.quote|sanitize)", type: "context_line" }],
|
|
5992
|
+
fix: { description: "Never interpolate any field that could be attacker-controlled into a shell command. Use execFile with argv array (no shell) or quote the input with shell-quote/shlex.quote." }
|
|
5993
|
+
},
|
|
5994
|
+
{
|
|
5995
|
+
id: "llm/openai-assistant-tool-no-allowlist",
|
|
5996
|
+
title: "OpenAI Assistants / Responses API Without Tool Allowlist",
|
|
5997
|
+
description: "This code uses the OpenAI Assistants API or Responses API with tools enabled and no allowlist check before executing the returned tool call. The Codex command-injection chain (CVE patched Feb 2026) and the ChatGPT data-exfil chain (Feb 2026) both relied on this \u2014 the agent decides what to call, the code does it.",
|
|
5998
|
+
severity: "high",
|
|
5999
|
+
confidence: "medium",
|
|
6000
|
+
cwe: "CWE-284",
|
|
6001
|
+
languages: ["javascript", "typescript", "python"],
|
|
6002
|
+
patterns: [
|
|
6003
|
+
{ regex: "required_action[\\s\\S]{0,100}submit_tool_outputs", type: "match" }
|
|
6004
|
+
],
|
|
6005
|
+
excludePatterns: [{ regex: "(?:allowedTools|toolRegistry|ALLOW_LIST|whitelist|permitted)", type: "context_line" }],
|
|
6006
|
+
fix: { description: "Maintain an explicit Set<string> of permitted tool names. Reject any tool call whose name isn't in the set. Log and alert on rejections." }
|
|
5492
6007
|
}
|
|
5493
6008
|
];
|
|
5494
6009
|
var HEADERS_RULES = [
|
|
@@ -5773,6 +6288,58 @@ var DEPS_RULES = [
|
|
|
5773
6288
|
],
|
|
5774
6289
|
excludePatterns: [{ regex: "(?:integrity|crossorigin)", type: "context_line" }],
|
|
5775
6290
|
fix: { description: "Add integrity and crossorigin attributes to all CDN-loaded scripts.", suggestion: '<script src="https://cdn.example.com/lib.js" integrity="sha384-..." crossorigin="anonymous"></script>' }
|
|
6291
|
+
},
|
|
6292
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
6293
|
+
// Round 7-vuln additions: known-malicious packages from the Clinejection
|
|
6294
|
+
// class of supply-chain attacks, extended slopsquatting list, no-lockfile.
|
|
6295
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
6296
|
+
{
|
|
6297
|
+
id: "deps/known-malicious-package",
|
|
6298
|
+
title: "Known-Malicious Package Detected",
|
|
6299
|
+
description: "This package was used in a confirmed supply-chain attack. Remove it immediately and rotate any credentials that were on the machine when it was installed.",
|
|
6300
|
+
severity: "critical",
|
|
6301
|
+
confidence: "high",
|
|
6302
|
+
cwe: "CWE-829",
|
|
6303
|
+
owasp: "A06:2021",
|
|
6304
|
+
languages: ["json"],
|
|
6305
|
+
patterns: [
|
|
6306
|
+
{ regex: '"(?:openclaw|postmark-mcp)"\\s*:', type: "match" },
|
|
6307
|
+
{ regex: '"cline"\\s*:\\s*"[^"]*2\\.3\\.0[^"]*"', type: "match" },
|
|
6308
|
+
{ regex: "package(?:-lock)?\\.json$", type: "file_path" }
|
|
6309
|
+
],
|
|
6310
|
+
fix: { description: "Remove the package. Rotate any tokens (npm publish, GitHub PAT, AI provider keys) that were active on the machine since install. Audit your shell history and ~/.npm cache. The Cline 2.3.0 incident shipped openclaw as a postinstall payload between 3:26am and 11:30am PT on 2026-02-17." }
|
|
6311
|
+
},
|
|
6312
|
+
{
|
|
6313
|
+
id: "deps/slopsquatting-risk-extended",
|
|
6314
|
+
title: "Likely AI-Hallucinated Package Name (Extended List)",
|
|
6315
|
+
description: "Extension of `deps/slopsquatting-risk` with names observed in the past 6 months of AI-generated code (Cursor, Lovable, Bolt, v0 traces). These names sound plausible \u2014 'react-query-helpers', 'nextjs-auth-utils' \u2014 but they didn't exist when the model trained. Squatters now own these names.",
|
|
6316
|
+
severity: "high",
|
|
6317
|
+
confidence: "low",
|
|
6318
|
+
cwe: "CWE-829",
|
|
6319
|
+
languages: ["json"],
|
|
6320
|
+
patterns: [
|
|
6321
|
+
{ regex: '"(?:react-query-helpers|nextjs-auth-utils|supabase-edge-utils|shadcn-ui-helpers|drizzle-helpers|hono-helpers|tanstack-router-utils|effect-helpers|zustand-helpers|tailwind-typography-extended|framer-motion-utils|radix-ui-helpers|trpc-helpers|prisma-edge-helpers|vercel-blob-utils|cloudflare-d1-helpers|nuxt-auth-utils|svelte-kit-helpers|astro-edge-helpers|solid-js-helpers)"\\s*:', type: "match" },
|
|
6322
|
+
{ regex: "package\\.json$", type: "file_path" }
|
|
6323
|
+
],
|
|
6324
|
+
fix: { description: "Verify on npmjs.com. Run `npm view <pkg>` \u2014 if the package is <30 days old, has one maintainer, and zero stars, do not install." }
|
|
6325
|
+
},
|
|
6326
|
+
{
|
|
6327
|
+
id: "deps/no-lockfile",
|
|
6328
|
+
title: "No Dependency Lockfile in Project",
|
|
6329
|
+
description: "This project's package.json has no companion package-lock.json / pnpm-lock.yaml / yarn.lock. Every install re-resolves versions, so a malicious update to any transitive dep ships on your next `npm install`. The Cline supply-chain attack window was 8 hours \u2014 without a lockfile, every install during that window pulled malware.",
|
|
6330
|
+
severity: "medium",
|
|
6331
|
+
confidence: "high",
|
|
6332
|
+
cwe: "CWE-1357",
|
|
6333
|
+
languages: ["json"],
|
|
6334
|
+
patterns: [
|
|
6335
|
+
{ regex: '"name"\\s*:', type: "match" },
|
|
6336
|
+
{ regex: "(?:^|/)package\\.json$", type: "file_path" }
|
|
6337
|
+
],
|
|
6338
|
+
requireSibling: {
|
|
6339
|
+
// Rule fires only if NONE of these lockfiles exist anywhere in the scan set.
|
|
6340
|
+
missing: ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb"]
|
|
6341
|
+
},
|
|
6342
|
+
fix: { description: "Commit a lockfile. `npm install --package-lock-only` or `pnpm install --lockfile-only`. Re-run on every dependency change. Set engine-strict and pin the package manager via `packageManager` in package.json." }
|
|
5776
6343
|
}
|
|
5777
6344
|
];
|
|
5778
6345
|
var CLIENT_RULES = [
|
|
@@ -5846,6 +6413,224 @@ var CLIENT_RULES = [
|
|
|
5846
6413
|
fix: { description: "Add CSRF protection: use SameSite=Strict cookies, verify a CSRF token header, or use a framework that handles this automatically (e.g. Next.js Server Actions have built-in CSRF protection)." }
|
|
5847
6414
|
}
|
|
5848
6415
|
];
|
|
6416
|
+
var AI_AGENT_RULES = [
|
|
6417
|
+
{
|
|
6418
|
+
id: "ai-agent/invisible-unicode-in-config",
|
|
6419
|
+
title: "AI Agent Config File Contains Invisible Unicode",
|
|
6420
|
+
description: "This agent config file contains zero-width, tag, or variation-selector Unicode characters that humans can't see but LLMs read as normal text. This is the standard technique for hiding prompt injection payloads inside .cursorrules / .windsurfrules / CLAUDE.md / AGENTS.md / copilot-instructions.md. If you didn't put them there, an attacker did.",
|
|
6421
|
+
severity: "critical",
|
|
6422
|
+
confidence: "high",
|
|
6423
|
+
cwe: "CWE-1007",
|
|
6424
|
+
owasp: "A03:2021",
|
|
6425
|
+
languages: ["markdown"],
|
|
6426
|
+
patterns: [
|
|
6427
|
+
// JS regex Unicode escape uses \u{...} with /u flag (PCRE \x{} doesn't work).
|
|
6428
|
+
// compileRegex auto-adds /u when it sees \u{...} braces in the source.
|
|
6429
|
+
{ regex: "[\\u{200B}-\\u{200F}\\u{2028}-\\u{202F}\\u{2060}-\\u{206F}\\u{FE00}-\\u{FE0F}\\u{E0000}-\\u{E007F}]", type: "match" },
|
|
6430
|
+
{ regex: "(?:\\.cursorrules|\\.windsurfrules|\\.clinerules|CLAUDE\\.md|AGENTS\\.md|copilot-instructions\\.md|\\.mdc|\\.cursor/rules/)", type: "file_path" }
|
|
6431
|
+
],
|
|
6432
|
+
fix: { description: "Open the file in an editor that visualizes invisible characters (VS Code with 'Render Whitespace' + the 'Gremlins' extension, or `cat -v`). Strip any character outside printable ASCII unless you intentionally need it. Add a pre-commit hook that rejects these ranges.", suggestion: "perl -CSDA -pe 's/[\\x{200B}-\\x{200F}\\x{2060}-\\x{206F}\\x{E0000}-\\x{E007F}]//g' -i .cursorrules" }
|
|
6433
|
+
},
|
|
6434
|
+
{
|
|
6435
|
+
id: "ai-agent/mcp-stdio-shell-command",
|
|
6436
|
+
title: "MCP Server Runs an Arbitrary Shell Command",
|
|
6437
|
+
description: "This MCP server config launches a STDIO process via a shell wrapper (sh -c, bash -c, eval, exec) or `npx -y` (downloads and runs arbitrary npm code). A poisoned tool description or a rug-pull update can inject extra arguments and run anything on your machine. The Windsurf zero-click RCE (CVE-2026-30615) used exactly this shape.",
|
|
6438
|
+
severity: "critical",
|
|
6439
|
+
confidence: "high",
|
|
6440
|
+
cwe: "CWE-78",
|
|
6441
|
+
owasp: "A03:2021",
|
|
6442
|
+
languages: ["json"],
|
|
6443
|
+
patterns: [
|
|
6444
|
+
{ regex: '"command"\\s*:\\s*"(?:sh|bash|zsh|cmd|powershell|pwsh|eval|exec)"', type: "match" },
|
|
6445
|
+
{ regex: '"args"\\s*:\\s*\\[\\s*"-c"', type: "match" },
|
|
6446
|
+
{ regex: "(?:mcp\\.json|mcp_settings\\.json|claude_desktop_config\\.json)$", type: "file_path" }
|
|
6447
|
+
],
|
|
6448
|
+
fix: { description: 'MCP servers should be invoked with their executable directly \u2014 `"command": "node"`, `"args": ["server.js"]` \u2014 never wrapped in a shell. If the server requires shell expansion, the server itself is wrong.' }
|
|
6449
|
+
},
|
|
6450
|
+
{
|
|
6451
|
+
id: "ai-agent/mcp-public-http-endpoint",
|
|
6452
|
+
title: "MCP Server Points to a Public HTTP URL",
|
|
6453
|
+
description: "This MCP server is configured to talk to an HTTP endpoint, not a local STDIO process, and the URL is on the public internet without TLS. Anything the server returns becomes context the agent acts on, so a network attacker can serve poisoned tool descriptions and trigger a confused-deputy attack.",
|
|
6454
|
+
severity: "high",
|
|
6455
|
+
confidence: "medium",
|
|
6456
|
+
cwe: "CWE-319",
|
|
6457
|
+
owasp: "A02:2021",
|
|
6458
|
+
languages: ["json"],
|
|
6459
|
+
patterns: [
|
|
6460
|
+
{ regex: '"url"\\s*:\\s*"http://(?!localhost|127\\.|0\\.0\\.0\\.0|::1)', type: "match" },
|
|
6461
|
+
{ regex: "(?:mcp\\.json|claude_desktop_config\\.json)$", type: "file_path" }
|
|
6462
|
+
],
|
|
6463
|
+
excludePatterns: [
|
|
6464
|
+
{ regex: "ngrok\\.io|ngrok-free\\.app|loca\\.lt", type: "context_line" }
|
|
6465
|
+
],
|
|
6466
|
+
fix: { description: "Require HTTPS for any MCP server you don't run locally. Pin the server's full URL including version path. Prefer STDIO with a locally installed binary for anything that handles credentials." }
|
|
6467
|
+
},
|
|
6468
|
+
{
|
|
6469
|
+
id: "ai-agent/cursor-auto-run-enabled",
|
|
6470
|
+
title: "Cursor Auto-Run / YOLO Mode Enabled",
|
|
6471
|
+
description: "Cursor's Auto-Run Mode (formerly YOLO Mode) lets the agent execute commands without per-call approval. Combined with prompt injection from an MCP server, a rules file, or a PR comment, this turns the IDE into a one-shot RCE. CVE-2026-22708 specifically bypasses the allowlist via shell built-ins when this mode is on.",
|
|
6472
|
+
severity: "high",
|
|
6473
|
+
confidence: "high",
|
|
6474
|
+
cwe: "CWE-269",
|
|
6475
|
+
languages: ["json"],
|
|
6476
|
+
patterns: [
|
|
6477
|
+
{ regex: '"cursor\\.(?:autoRun|yoloMode|composerAutoRun|chat\\.autoExecute|agent\\.autoRun|agent\\.skipApproval)"\\s*:\\s*true', type: "match" },
|
|
6478
|
+
{ regex: '"agent"\\s*:\\s*\\{[^}]*"autoApprove"\\s*:\\s*true', type: "match" }
|
|
6479
|
+
],
|
|
6480
|
+
fix: { description: "Disable Auto-Run for any repo that contains code you didn't write yourself. If you must keep it on, also disable shell built-ins in the allowlist and review every MCP server in .cursor/mcp.json first." }
|
|
6481
|
+
},
|
|
6482
|
+
{
|
|
6483
|
+
id: "ai-agent/agent-allowlist-disabled",
|
|
6484
|
+
title: "Agent Tool Allowlist Disabled or Empty",
|
|
6485
|
+
description: "This agent config disables the tool allowlist or sets it to allow everything. The allowlist is the last layer between a prompt injection and shell access \u2014 turning it off means any malicious instruction (in a file, an MCP tool description, a PR comment) becomes arbitrary code execution.",
|
|
6486
|
+
severity: "high",
|
|
6487
|
+
confidence: "medium",
|
|
6488
|
+
cwe: "CWE-284",
|
|
6489
|
+
languages: ["json"],
|
|
6490
|
+
patterns: [
|
|
6491
|
+
{ regex: '"allowlist"\\s*:\\s*(?:false|null|\\[\\s*"\\*"\\s*\\])', type: "match" },
|
|
6492
|
+
{ regex: '"toolApproval"\\s*:\\s*"never"', type: "match" }
|
|
6493
|
+
],
|
|
6494
|
+
fix: { description: "Keep the allowlist on. Add specific tool names you genuinely need (read_file, edit_file) and leave shell, web fetch, and MCP-write tools off until you have a reason to enable them per session." }
|
|
6495
|
+
},
|
|
6496
|
+
{
|
|
6497
|
+
id: "ai-agent/auto-approve-tools",
|
|
6498
|
+
title: "Agent Configured to Auto-Approve All Tool Calls",
|
|
6499
|
+
description: "This config tells the agent to approve every tool call automatically. That removes the human-in-the-loop check that catches prompt injection mid-attack. The 'Comment and Control' PR-injection family relies on this \u2014 without auto-approve, the user sees the exfil command and stops it.",
|
|
6500
|
+
severity: "high",
|
|
6501
|
+
confidence: "medium",
|
|
6502
|
+
cwe: "CWE-284",
|
|
6503
|
+
languages: ["json", "yaml", "markdown", "shell"],
|
|
6504
|
+
patterns: [
|
|
6505
|
+
{ regex: "(?:auto[_-]?approve|skipConfirmation|autoConfirm|trustedAlways)\\s*[:=]\\s*true", type: "match" },
|
|
6506
|
+
{ regex: "--dangerously-skip-permissions", type: "match" }
|
|
6507
|
+
],
|
|
6508
|
+
fix: { description: "Remove the auto-approve flag for any agent that touches a CI environment, untrusted repo, or anything with real credentials. The 5 seconds of friction is the entire point." }
|
|
6509
|
+
},
|
|
6510
|
+
{
|
|
6511
|
+
id: "ai-agent/ci-agent-untrusted-pr-input",
|
|
6512
|
+
title: "CI Agent Reads PR Title or Body Without Sanitizing",
|
|
6513
|
+
description: "This GitHub Action passes github.event.pull_request.title, .body, or .head_ref directly to an AI agent (Claude Code, Copilot Agent, Gemini CLI, Codex). An attacker opens a PR with prompt-injection in the title and exfiltrates ANTHROPIC_API_KEY / GITHUB_TOKEN. This is the 'Comment and Control' / CVE-2026-35020 family.",
|
|
6514
|
+
severity: "critical",
|
|
6515
|
+
confidence: "medium",
|
|
6516
|
+
cwe: "CWE-77",
|
|
6517
|
+
owasp: "A03:2021",
|
|
6518
|
+
languages: ["yaml"],
|
|
6519
|
+
patterns: [
|
|
6520
|
+
{ regex: "(?:claude-code|copilot|gemini|aider|devin|codex)[\\s\\S]{0,200}\\$\\{\\{\\s*github\\.event\\.pull_request\\.(?:title|body|head_ref)", type: "match" },
|
|
6521
|
+
{ regex: "\\.github/workflows/", type: "file_path" }
|
|
6522
|
+
],
|
|
6523
|
+
fix: { description: "Never interpolate PR-controlled fields directly into an agent prompt or shell line. Pass them through an env var, then have the agent treat that env var as untrusted data (not instructions). Block any prompt-injection-shaped string before invoking the agent." }
|
|
6524
|
+
},
|
|
6525
|
+
{
|
|
6526
|
+
id: "ai-agent/ci-agent-pull-request-fork",
|
|
6527
|
+
title: "CI Agent Triggers on pull_request from Forks",
|
|
6528
|
+
description: "This workflow uses pull_request and runs an AI agent. Forked PRs run in your workflow context with read access to secrets if the action requests them \u2014 combined with PR-body prompt injection, this is the documented exfiltration vector.",
|
|
6529
|
+
severity: "high",
|
|
6530
|
+
confidence: "low",
|
|
6531
|
+
cwe: "CWE-269",
|
|
6532
|
+
languages: ["yaml"],
|
|
6533
|
+
patterns: [
|
|
6534
|
+
{ regex: "on\\s*:[\\s\\S]{0,80}pull_request(?:_target)?", type: "match" },
|
|
6535
|
+
{ regex: "(?:claude-code|copilot|gemini|aider|devin)", type: "context_line" },
|
|
6536
|
+
{ regex: "\\.github/workflows/", type: "file_path" }
|
|
6537
|
+
],
|
|
6538
|
+
fix: { description: "Gate AI-agent jobs on `if: github.event.pull_request.head.repo.full_name == github.repository` so forks can't trigger them with secrets attached. Or move the agent to a manually-triggered workflow_dispatch after a maintainer reviews the PR." }
|
|
6539
|
+
},
|
|
6540
|
+
{
|
|
6541
|
+
id: "ai-agent/github-action-injection-from-pr",
|
|
6542
|
+
title: "PR Title or Body Interpolated into Shell Step",
|
|
6543
|
+
description: "This workflow uses ${{ github.event.pull_request.title }} or similar inside a `run:` block. GitHub interpolates that into the shell before any escaping \u2014 an attacker with a PR title containing backticks runs commands on your runner. Independent of AI agents but urgent because AI-agent workflows tend to ship in this shape.",
|
|
6544
|
+
severity: "critical",
|
|
6545
|
+
confidence: "high",
|
|
6546
|
+
cwe: "CWE-78",
|
|
6547
|
+
owasp: "A03:2021",
|
|
6548
|
+
languages: ["yaml"],
|
|
6549
|
+
patterns: [
|
|
6550
|
+
{ regex: "run\\s*:[\\s\\S]{0,400}\\$\\{\\{\\s*github\\.event\\.(?:pull_request|issue|comment|review|release|discussion|head_commit|commits)\\.[a-z_]+(?:\\.[a-z_]+)?", type: "match" },
|
|
6551
|
+
{ regex: "\\.github/workflows/", type: "file_path" }
|
|
6552
|
+
],
|
|
6553
|
+
fix: { description: 'Move the untrusted value into an env var (env: PR_TITLE: ${{ github.event.pull_request.title }}), then reference "$PR_TITLE" from the shell. The runner expands env vars after the shell parses the script, so injection becomes inert.' }
|
|
6554
|
+
},
|
|
6555
|
+
{
|
|
6556
|
+
id: "ai-agent/agent-config-not-gitignored",
|
|
6557
|
+
title: "Agent Config Path Suggests Workspace File Was Committed",
|
|
6558
|
+
description: "Files like .cursor/mcp.json, .claude/settings.json, or .windsurf/config.json often contain MCP credentials, API keys, and machine-specific paths. If they're tracked by git, those secrets ship to anyone with repo access. (This rule signals a path-level concern; combine with `config/agent-config-tracked` for a stronger signal.)",
|
|
6559
|
+
severity: "low",
|
|
6560
|
+
confidence: "low",
|
|
6561
|
+
cwe: "CWE-538",
|
|
6562
|
+
languages: ["json"],
|
|
6563
|
+
patterns: [
|
|
6564
|
+
{ regex: "(?:^|/)(?:\\.cursor|\\.claude|\\.windsurf|\\.cline|\\.continue|\\.aider)/", type: "file_path" },
|
|
6565
|
+
{ regex: "\\S", type: "match" }
|
|
6566
|
+
],
|
|
6567
|
+
excludePatterns: [
|
|
6568
|
+
{ regex: "\\.gitignore$", type: "file_path" }
|
|
6569
|
+
],
|
|
6570
|
+
fix: { description: "Add .cursor/, .claude/, .windsurf/, .cline/, .continue/, .aider* to .gitignore. Move anything secret out of the tracked config and into a local-only override." }
|
|
6571
|
+
},
|
|
6572
|
+
// ── Tier 2 additions (§4 of handoff) ────────────────────────────────────
|
|
6573
|
+
{
|
|
6574
|
+
id: "ai-agent/pwn-request-checkout",
|
|
6575
|
+
title: "GitHub Action Uses pull_request_target + Checks Out PR Code",
|
|
6576
|
+
description: "This workflow uses pull_request_target (which runs in the base repo's context with secrets) AND checks out the PR's head ref. Attacker opens a PR, the workflow runs their code with your secrets in env, attacker exfiltrates. This is the 'Pwn Request' attack \u2014 researchers have compromised Microsoft, Google, and Nvidia repos with exactly this pattern.",
|
|
6577
|
+
severity: "critical",
|
|
6578
|
+
confidence: "high",
|
|
6579
|
+
cwe: "CWE-269",
|
|
6580
|
+
owasp: "A01:2021",
|
|
6581
|
+
languages: ["yaml"],
|
|
6582
|
+
patterns: [
|
|
6583
|
+
{ regex: "pull_request_target", type: "match" },
|
|
6584
|
+
{ regex: "actions/checkout@[^\\s]+[\\s\\S]{0,200}ref\\s*:\\s*\\$\\{\\{\\s*github\\.event\\.pull_request\\.head\\.(?:sha|ref)", type: "match" },
|
|
6585
|
+
{ regex: "\\.github/workflows/", type: "file_path" }
|
|
6586
|
+
],
|
|
6587
|
+
fix: { description: "Either switch to pull_request (loses secrets \u2014 that's the point) or split into two workflows. The privileged workflow runs only on the base branch (commenting, labeling); the build workflow runs on pull_request without secrets and reports back via artifact." }
|
|
6588
|
+
},
|
|
6589
|
+
{
|
|
6590
|
+
id: "ai-agent/workflow-write-all-permissions",
|
|
6591
|
+
title: "Workflow Grants Write-All Permissions to an AI-Agent Job",
|
|
6592
|
+
description: "This workflow runs an AI agent (Claude Code, Copilot Agent, Codex) and grants `permissions: write-all` or doesn't set permissions: (which defaults to write on classic-token repos). A prompt-injection from PR content turns into write access to the repo, releases, and packages.",
|
|
6593
|
+
severity: "high",
|
|
6594
|
+
confidence: "medium",
|
|
6595
|
+
cwe: "CWE-269",
|
|
6596
|
+
languages: ["yaml"],
|
|
6597
|
+
patterns: [
|
|
6598
|
+
{ regex: "permissions\\s*:\\s*write-all", type: "match" },
|
|
6599
|
+
{ regex: "(?:claude-code|copilot|codex|gemini|aider)", type: "context_line" },
|
|
6600
|
+
{ regex: "\\.github/workflows/", type: "file_path" }
|
|
6601
|
+
],
|
|
6602
|
+
fix: { description: "Set explicit permissions: block listing only what the job needs. For an agent that comments on PRs: `permissions: { contents: read, pull-requests: write }`." }
|
|
6603
|
+
},
|
|
6604
|
+
{
|
|
6605
|
+
id: "ai-agent/jetbrains-junie-json-schema-remote",
|
|
6606
|
+
title: "JSON Schema Loaded from Untrusted Remote URL",
|
|
6607
|
+
description: "JetBrains IDEs auto-load $schema URLs in JSON files. JetBrains Junie CVE-2025-58335 abused this \u2014 a malicious JSON in your repo points $schema at an attacker-controlled host, the IDE fetches it, and follow-on behavior (settings overwrite, prompt context contamination) opens the agent up.",
|
|
6608
|
+
severity: "medium",
|
|
6609
|
+
confidence: "medium",
|
|
6610
|
+
cwe: "CWE-918",
|
|
6611
|
+
languages: ["json"],
|
|
6612
|
+
patterns: [
|
|
6613
|
+
{ regex: '"\\$schema"\\s*:\\s*"http://(?!localhost|127\\.)', type: "match" },
|
|
6614
|
+
{ regex: '"\\$schema"\\s*:\\s*"https?://(?!schemas\\.(?:jetbrains|microsoft|github)\\.|json\\.schemastore\\.org|raw\\.githubusercontent\\.com/SchemaStore/)', type: "match" }
|
|
6615
|
+
],
|
|
6616
|
+
fix: { description: "Only reference $schema URLs from trusted hosts (schemastore.org, jetbrains.com, github.com). Audit any custom schema URL in your repo \u2014 if it's not yours, remove it." }
|
|
6617
|
+
},
|
|
6618
|
+
{
|
|
6619
|
+
id: "ai-agent/cursor-mdc-rules-directory-presence",
|
|
6620
|
+
title: "Cursor .cursor/rules/*.mdc Files Present \u2014 Review Recommended",
|
|
6621
|
+
description: "Cursor's 2026 rules format is .cursor/rules/<name>.mdc (replacing the single .cursorrules file). Each .mdc carries frontmatter + instructions the agent reads. These files are prime targets for prompt injection and invisible Unicode payloads. This rule is an awareness flag \u2014 every .mdc file in a freshly cloned repo deserves a human read before opening Cursor.",
|
|
6622
|
+
severity: "low",
|
|
6623
|
+
confidence: "high",
|
|
6624
|
+
cwe: "CWE-1188",
|
|
6625
|
+
languages: ["markdown"],
|
|
6626
|
+
patterns: [
|
|
6627
|
+
{ regex: "\\S", type: "match" },
|
|
6628
|
+
// any non-empty content
|
|
6629
|
+
{ regex: "\\.cursor/rules/.*\\.mdc$", type: "file_path" }
|
|
6630
|
+
],
|
|
6631
|
+
fix: { description: "Read every .mdc rule file before running Cursor on a new repo. If you didn't write the rule, treat it as untrusted instructions until you've verified it. Combine with `ai-agent/invisible-unicode-in-config` to catch the hidden-payload variant." }
|
|
6632
|
+
}
|
|
6633
|
+
];
|
|
5849
6634
|
var ALL_RULES = [
|
|
5850
6635
|
...SECRET_RULES,
|
|
5851
6636
|
...INJECTION_RULES,
|
|
@@ -5860,6 +6645,7 @@ var ALL_RULES = [
|
|
|
5860
6645
|
...HEADERS_RULES,
|
|
5861
6646
|
...DEPS_RULES,
|
|
5862
6647
|
...CLIENT_RULES,
|
|
6648
|
+
...AI_AGENT_RULES,
|
|
5863
6649
|
...ALL_FRAMEWORK_RULES
|
|
5864
6650
|
];
|
|
5865
6651
|
var CATEGORY_MAP = {
|
|
@@ -5876,6 +6662,7 @@ var CATEGORY_MAP = {
|
|
|
5876
6662
|
headers: HEADERS_RULES,
|
|
5877
6663
|
deps: DEPS_RULES,
|
|
5878
6664
|
client: CLIENT_RULES,
|
|
6665
|
+
"ai-agent": AI_AGENT_RULES,
|
|
5879
6666
|
nextjs: NEXTJS_RULES,
|
|
5880
6667
|
express: EXPRESS_RULES,
|
|
5881
6668
|
django: DJANGO_RULES,
|
|
@@ -5911,10 +6698,29 @@ function extractSnippet(content, line, contextLines = 5) {
|
|
|
5911
6698
|
return `${marker} ${lineNum} | ${l}`;
|
|
5912
6699
|
}).join("\n");
|
|
5913
6700
|
}
|
|
5914
|
-
function isCommentLine(line) {
|
|
6701
|
+
function isCommentLine(line, language) {
|
|
5915
6702
|
const trimmed = line.trim();
|
|
6703
|
+
if (language === "markdown") {
|
|
6704
|
+
return trimmed.startsWith("<!--");
|
|
6705
|
+
}
|
|
6706
|
+
if (language === "yaml" || language === "shell") {
|
|
6707
|
+
return trimmed.startsWith("#");
|
|
6708
|
+
}
|
|
6709
|
+
if (language === "sql") {
|
|
6710
|
+
return trimmed.startsWith("--") || trimmed.startsWith("/*") || trimmed.startsWith("* ") || trimmed.startsWith("*/") || trimmed === "*";
|
|
6711
|
+
}
|
|
6712
|
+
if (language === "json") {
|
|
6713
|
+
return false;
|
|
6714
|
+
}
|
|
5916
6715
|
return trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("/*") || trimmed.startsWith("* ") || trimmed.startsWith("*/") || trimmed === "*" || trimmed.startsWith("<!--");
|
|
5917
6716
|
}
|
|
6717
|
+
function compileRegex(source, flags) {
|
|
6718
|
+
let finalFlags = flags;
|
|
6719
|
+
if (/\\u\{[0-9A-Fa-f]+\}/.test(source)) {
|
|
6720
|
+
if (!finalFlags.includes("u")) finalFlags += "u";
|
|
6721
|
+
}
|
|
6722
|
+
return new RegExp(source, finalFlags);
|
|
6723
|
+
}
|
|
5918
6724
|
function isInsideStringLiteral(line, pattern) {
|
|
5919
6725
|
const match = pattern.exec(line);
|
|
5920
6726
|
if (!match) return false;
|
|
@@ -5926,29 +6732,56 @@ function isInsideStringLiteral(line, pattern) {
|
|
|
5926
6732
|
}
|
|
5927
6733
|
return backtickCount % 2 === 1;
|
|
5928
6734
|
}
|
|
6735
|
+
function isJsxTextContent(line) {
|
|
6736
|
+
const trimmed = line.trim();
|
|
6737
|
+
if (/^\w+\s*[:=]\s*["'`]/.test(trimmed) && trimmed.length > 80) {
|
|
6738
|
+
const nonCodeChars = trimmed.replace(/[a-zA-Z\s.,!?;:'"()\-]/g, "").length;
|
|
6739
|
+
if (nonCodeChars / trimmed.length < 0.15) return true;
|
|
6740
|
+
}
|
|
6741
|
+
if (/>[^<]{40,}</.test(trimmed)) return true;
|
|
6742
|
+
if (/["'`][A-Z][^"'`]{60,}["'`]/.test(trimmed)) {
|
|
6743
|
+
return true;
|
|
6744
|
+
}
|
|
6745
|
+
return false;
|
|
6746
|
+
}
|
|
5929
6747
|
function matchRule(rule, file) {
|
|
5930
6748
|
if (!rule.languages.includes("*") && !rule.languages.includes(file.language)) {
|
|
5931
6749
|
return [];
|
|
5932
6750
|
}
|
|
5933
6751
|
const isSecretRule = rule.id.startsWith("secrets/");
|
|
5934
6752
|
if (!isSecretRule) {
|
|
5935
|
-
const
|
|
5936
|
-
if (
|
|
6753
|
+
const contentPaths = /(?:\/docs\/|\/blog\/|\/for\/|\/examples\/|\/fixtures\/|\/tutorials?\/|\/guides?\/|__tests__\/fixtures)/i;
|
|
6754
|
+
if (contentPaths.test(file.path)) return [];
|
|
6755
|
+
}
|
|
6756
|
+
const filePathPatterns = rule.patterns.filter((p) => p.type === "file_path");
|
|
6757
|
+
if (filePathPatterns.length > 0) {
|
|
6758
|
+
const allFilePathMatch = filePathPatterns.every(
|
|
6759
|
+
(p) => compileRegex(p.regex, "i").test(file.path)
|
|
6760
|
+
);
|
|
6761
|
+
if (!allFilePathMatch) return [];
|
|
6762
|
+
}
|
|
6763
|
+
const contextLinePatterns = rule.patterns.filter((p) => p.type === "context_line");
|
|
6764
|
+
if (contextLinePatterns.length > 0) {
|
|
6765
|
+
const allContextMatch = contextLinePatterns.every(
|
|
6766
|
+
(p) => compileRegex(p.regex, "i").test(file.content)
|
|
6767
|
+
);
|
|
6768
|
+
if (!allContextMatch) return [];
|
|
5937
6769
|
}
|
|
5938
6770
|
const findings = [];
|
|
5939
6771
|
const lines = file.content.split("\n");
|
|
5940
6772
|
for (const pattern of rule.patterns) {
|
|
5941
6773
|
if (pattern.type !== "match") continue;
|
|
5942
|
-
const regex =
|
|
6774
|
+
const regex = compileRegex(pattern.regex, "gi");
|
|
5943
6775
|
for (let i = 0; i < lines.length; i++) {
|
|
5944
6776
|
const line = lines[i];
|
|
5945
6777
|
if (!regex.test(line)) continue;
|
|
5946
6778
|
regex.lastIndex = 0;
|
|
5947
|
-
if (!isSecretRule && isCommentLine(line)) continue;
|
|
5948
|
-
if (!isSecretRule && isInsideStringLiteral(line,
|
|
6779
|
+
if (!isSecretRule && isCommentLine(line, file.language)) continue;
|
|
6780
|
+
if (!isSecretRule && isInsideStringLiteral(line, compileRegex(pattern.regex, "gi"))) continue;
|
|
6781
|
+
if (!isSecretRule && isJsxTextContent(line)) continue;
|
|
5949
6782
|
if (rule.excludePatterns?.length) {
|
|
5950
6783
|
const excluded = rule.excludePatterns.some((ep) => {
|
|
5951
|
-
const exRegex =
|
|
6784
|
+
const exRegex = compileRegex(ep.regex, "i");
|
|
5952
6785
|
if (ep.type === "context_line") {
|
|
5953
6786
|
return exRegex.test(line);
|
|
5954
6787
|
}
|
|
@@ -5982,8 +6815,21 @@ function matchRule(rule, file) {
|
|
|
5982
6815
|
}
|
|
5983
6816
|
function runRules(rules, files) {
|
|
5984
6817
|
const findings = [];
|
|
6818
|
+
const filePathSet = new Set(files.map((f) => f.path));
|
|
6819
|
+
const suppressedByMissingSibling = /* @__PURE__ */ new Set();
|
|
6820
|
+
for (const rule of rules) {
|
|
6821
|
+
if (!rule.requireSibling?.missing?.length) continue;
|
|
6822
|
+
const anyPresent = rule.requireSibling.missing.some((sibling) => {
|
|
6823
|
+
for (const filePath of filePathSet) {
|
|
6824
|
+
if (filePath === sibling || filePath.endsWith("/" + sibling)) return true;
|
|
6825
|
+
}
|
|
6826
|
+
return false;
|
|
6827
|
+
});
|
|
6828
|
+
if (anyPresent) suppressedByMissingSibling.add(rule.id);
|
|
6829
|
+
}
|
|
5985
6830
|
for (const file of files) {
|
|
5986
6831
|
for (const rule of rules) {
|
|
6832
|
+
if (suppressedByMissingSibling.has(rule.id)) continue;
|
|
5987
6833
|
findings.push(...matchRule(rule, file));
|
|
5988
6834
|
}
|
|
5989
6835
|
}
|
|
@@ -6839,12 +7685,15 @@ function detectPlatform(files) {
|
|
|
6839
7685
|
if (lower.endsWith("package.json") && content.includes('"v0"')) {
|
|
6840
7686
|
signals.v0.push("v0 dependency in package.json");
|
|
6841
7687
|
}
|
|
6842
|
-
if (content.includes("base44") || content.includes("Base44")) {
|
|
6843
|
-
signals.base44.push("base44 reference in source");
|
|
6844
|
-
}
|
|
6845
7688
|
if (lower.includes("base44.config") || lower.includes(".base44")) {
|
|
6846
7689
|
signals.base44.push("base44 config file");
|
|
6847
7690
|
}
|
|
7691
|
+
if (/from\s+['"]@?base44\b/i.test(content) || /require\s*\(\s*['"]@?base44\b/i.test(content)) {
|
|
7692
|
+
signals.base44.push("base44 SDK import in source");
|
|
7693
|
+
}
|
|
7694
|
+
if (/https?:\/\/[^"'\s]*base44\.app/i.test(content)) {
|
|
7695
|
+
signals.base44.push("base44.app URL in source");
|
|
7696
|
+
}
|
|
6848
7697
|
}
|
|
6849
7698
|
const scores = Object.entries(signals).filter(([key]) => key !== "manual").map(([platform, sigs]) => ({
|
|
6850
7699
|
platform,
|
|
@@ -7408,23 +8257,22 @@ async function loginCommand(options) {
|
|
|
7408
8257
|
const profile = await fetchProfile(apiUrl, pollData.token);
|
|
7409
8258
|
if (profile) {
|
|
7410
8259
|
tokenData.tier = profile.tier;
|
|
7411
|
-
tokenData.cliTier = profile.cliTier;
|
|
7412
8260
|
tokenData.aiQuota = profile.aiQuota;
|
|
7413
8261
|
}
|
|
7414
8262
|
storeToken(tokenData);
|
|
7415
8263
|
pollSpinner.succeed(
|
|
7416
8264
|
chalk3.green(`Logged in as ${chalk3.bold(pollData.email)}`)
|
|
7417
8265
|
);
|
|
7418
|
-
if (profile?.
|
|
8266
|
+
if (profile?.tier && profile.tier !== "free") {
|
|
7419
8267
|
console.log(
|
|
7420
8268
|
chalk3.cyan(
|
|
7421
|
-
`
|
|
8269
|
+
` Plan: ${profile.tier} (${profile.aiQuota.remaining} AI scans remaining)`
|
|
7422
8270
|
)
|
|
7423
8271
|
);
|
|
7424
8272
|
} else {
|
|
7425
8273
|
console.log(
|
|
7426
8274
|
chalk3.dim(
|
|
7427
|
-
"
|
|
8275
|
+
" Free plan. Upgrade at ship-safe.co/pricing for AI-powered scanning."
|
|
7428
8276
|
)
|
|
7429
8277
|
);
|
|
7430
8278
|
}
|
|
@@ -7498,20 +8346,17 @@ async function whoamiCommand(options) {
|
|
|
7498
8346
|
storeToken({
|
|
7499
8347
|
...token,
|
|
7500
8348
|
tier: profile.tier,
|
|
7501
|
-
cliTier: profile.cliTier,
|
|
7502
8349
|
aiQuota: profile.aiQuota
|
|
7503
8350
|
});
|
|
7504
8351
|
spinner.stop();
|
|
7505
8352
|
console.log(chalk3.bold("\nShipSafe Account\n"));
|
|
7506
|
-
console.log(` Email:
|
|
7507
|
-
console.log(`
|
|
7508
|
-
if (profile.
|
|
7509
|
-
console.log(` CLI plan: ${chalk3.green(profile.cliTier)}`);
|
|
8353
|
+
console.log(` Email: ${chalk3.cyan(profile.email)}`);
|
|
8354
|
+
console.log(` Plan: ${chalk3.cyan(profile.tier)}`);
|
|
8355
|
+
if (profile.tier && profile.tier !== "free") {
|
|
7510
8356
|
console.log(
|
|
7511
8357
|
` AI scans: ${chalk3.cyan(`${profile.aiQuota.used}/${profile.aiQuota.limit}`)} used this month (${chalk3.green(`${profile.aiQuota.remaining}`)} remaining)`
|
|
7512
8358
|
);
|
|
7513
8359
|
} else {
|
|
7514
|
-
console.log(` CLI plan: ${chalk3.dim("none")}`);
|
|
7515
8360
|
console.log(
|
|
7516
8361
|
chalk3.dim(" Upgrade at ship-safe.co/pricing for AI-powered scanning.")
|
|
7517
8362
|
);
|
|
@@ -7520,8 +8365,8 @@ async function whoamiCommand(options) {
|
|
|
7520
8365
|
} else {
|
|
7521
8366
|
spinner.stop();
|
|
7522
8367
|
console.log(chalk3.green(`Logged in as ${chalk3.bold(token.email)}`));
|
|
7523
|
-
if (token.
|
|
7524
|
-
console.log(chalk3.cyan(`
|
|
8368
|
+
if (token.tier) {
|
|
8369
|
+
console.log(chalk3.cyan(` Plan: ${token.tier}`));
|
|
7525
8370
|
}
|
|
7526
8371
|
}
|
|
7527
8372
|
}
|
|
@@ -7755,21 +8600,50 @@ async function scanCommand(targetPath, options) {
|
|
|
7755
8600
|
spinner.succeed(
|
|
7756
8601
|
chalk5.green(`Scan complete \u2014 ${result.findings.length} findings in ${result.durationMs}ms`)
|
|
7757
8602
|
);
|
|
7758
|
-
if (tokenData?.
|
|
8603
|
+
if (tokenData?.tier === "growth" || tokenData?.tier === "badge" || tokenData?.tier === "shield") {
|
|
7759
8604
|
const aiSpinner = ora2({
|
|
7760
|
-
text: chalk5.dim("Running AI-powered deep analysis..."),
|
|
8605
|
+
text: chalk5.dim("Running AI-powered deep analysis... (this may take 1-3 minutes)"),
|
|
7761
8606
|
color: "yellow",
|
|
7762
8607
|
spinner: "dots12"
|
|
7763
8608
|
}).start();
|
|
8609
|
+
const stages = [
|
|
8610
|
+
"Uploading files to AI scanner...",
|
|
8611
|
+
"Analyzing code with Claude AI...",
|
|
8612
|
+
"Detecting auth & logic vulnerabilities...",
|
|
8613
|
+
"Checking business logic flaws...",
|
|
8614
|
+
"Generating plain-English report..."
|
|
8615
|
+
];
|
|
8616
|
+
let stageIdx = 0;
|
|
8617
|
+
const progressInterval = setInterval(() => {
|
|
8618
|
+
stageIdx = Math.min(stageIdx + 1, stages.length - 1);
|
|
8619
|
+
aiSpinner.text = chalk5.dim(`${stages[stageIdx]} (${(stageIdx + 1) * 20}%)`);
|
|
8620
|
+
}, 15e3);
|
|
7764
8621
|
try {
|
|
8622
|
+
const MAX_AI_FILES = 100;
|
|
8623
|
+
const MAX_FILE_BYTES = 1e5;
|
|
8624
|
+
const MAX_TOTAL_BYTES = 4 * 1024 * 1024;
|
|
8625
|
+
let totalBytes = 0;
|
|
8626
|
+
const aiFiles = [];
|
|
8627
|
+
for (const f of files) {
|
|
8628
|
+
const size = new TextEncoder().encode(f.content).byteLength;
|
|
8629
|
+
if (size > MAX_FILE_BYTES) continue;
|
|
8630
|
+
if (totalBytes + size > MAX_TOTAL_BYTES) break;
|
|
8631
|
+
totalBytes += size;
|
|
8632
|
+
aiFiles.push(f);
|
|
8633
|
+
if (aiFiles.length >= MAX_AI_FILES) break;
|
|
8634
|
+
}
|
|
8635
|
+
aiSpinner.text = chalk5.dim(`Uploading ${aiFiles.length} files to AI scanner...`);
|
|
7765
8636
|
const aiRes = await fetch(`${options.apiUrl}/api/cli/ai-scan`, {
|
|
7766
8637
|
method: "POST",
|
|
7767
8638
|
headers: {
|
|
7768
8639
|
"Content-Type": "application/json",
|
|
7769
8640
|
Authorization: `Bearer ${tokenData.token}`
|
|
7770
8641
|
},
|
|
7771
|
-
body: JSON.stringify({ files })
|
|
8642
|
+
body: JSON.stringify({ files: aiFiles }),
|
|
8643
|
+
signal: AbortSignal.timeout(3e5)
|
|
8644
|
+
// 5 minute timeout
|
|
7772
8645
|
});
|
|
8646
|
+
clearInterval(progressInterval);
|
|
7773
8647
|
if (aiRes.ok) {
|
|
7774
8648
|
const aiData = await aiRes.json();
|
|
7775
8649
|
const aiFindings = aiData.findings.map((f) => ({
|
|
@@ -7805,8 +8679,9 @@ async function scanCommand(targetPath, options) {
|
|
|
7805
8679
|
`AI analysis: ${aiFindings.length} additional findings`
|
|
7806
8680
|
)
|
|
7807
8681
|
);
|
|
8682
|
+
const isUnlimited = !aiData.quota.limit || aiData.quota.limit < 0 || aiData.quota.limit >= 999999;
|
|
7808
8683
|
printInfo(
|
|
7809
|
-
`AI scans: ${aiData.quota.used}/${aiData.quota.limit} used this month (${aiData.quota.remaining} remaining)`
|
|
8684
|
+
isUnlimited ? `AI scans: ${aiData.quota.used} used this month (unlimited plan)` : `AI scans: ${aiData.quota.used}/${aiData.quota.limit} used this month (${aiData.quota.remaining} remaining)`
|
|
7810
8685
|
);
|
|
7811
8686
|
} else if (aiRes.status === 429) {
|
|
7812
8687
|
aiSpinner.warn(
|
|
@@ -7826,10 +8701,14 @@ async function scanCommand(targetPath, options) {
|
|
|
7826
8701
|
chalk5.yellow(`AI analysis unavailable (HTTP ${aiRes.status}). ${errBody || "Showing rule-based results only."}`)
|
|
7827
8702
|
);
|
|
7828
8703
|
}
|
|
7829
|
-
} catch {
|
|
8704
|
+
} catch (fetchErr) {
|
|
8705
|
+
clearInterval(progressInterval);
|
|
8706
|
+
const errMsg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
|
|
7830
8707
|
aiSpinner.warn(
|
|
7831
|
-
chalk5.yellow(
|
|
8708
|
+
chalk5.yellow(`Could not reach AI scanning service: ${errMsg}`)
|
|
7832
8709
|
);
|
|
8710
|
+
} finally {
|
|
8711
|
+
clearInterval(progressInterval);
|
|
7833
8712
|
}
|
|
7834
8713
|
}
|
|
7835
8714
|
const ignoredRules = loadIgnoredRules(resolvedPath);
|
|
@@ -7855,6 +8734,12 @@ async function scanCommand(targetPath, options) {
|
|
|
7855
8734
|
const diff = diffWithPrevious(resolvedPath, result.findings);
|
|
7856
8735
|
saveScanHistory(resolvedPath, result.findings);
|
|
7857
8736
|
const isLoggedIn = !!getStoredToken();
|
|
8737
|
+
if (options.sarifFile) {
|
|
8738
|
+
writeFileSync4(options.sarifFile, formatSarifOutput(result), "utf-8");
|
|
8739
|
+
}
|
|
8740
|
+
if (options.jsonFile) {
|
|
8741
|
+
writeFileSync4(options.jsonFile, formatJsonOutput(result), "utf-8");
|
|
8742
|
+
}
|
|
7858
8743
|
switch (options.output) {
|
|
7859
8744
|
case "json":
|
|
7860
8745
|
console.log(formatJsonOutput(result));
|
|
@@ -8090,14 +8975,14 @@ function getVersion() {
|
|
|
8090
8975
|
for (let i = 0; i < 5; i++) {
|
|
8091
8976
|
try {
|
|
8092
8977
|
const pkg = JSON.parse(readFileSync6(join5(dir, "package.json"), "utf-8"));
|
|
8093
|
-
if (pkg.
|
|
8978
|
+
if (pkg.version) return pkg.version;
|
|
8094
8979
|
} catch {
|
|
8095
8980
|
}
|
|
8096
8981
|
dir = dirname(dir);
|
|
8097
8982
|
}
|
|
8098
8983
|
} catch {
|
|
8099
8984
|
}
|
|
8100
|
-
return "
|
|
8985
|
+
return "0.0.0-dev";
|
|
8101
8986
|
}
|
|
8102
8987
|
function validateApiUrl(value) {
|
|
8103
8988
|
let parsed;
|
|
@@ -8117,7 +9002,7 @@ program.command("scan").description("Scan a directory or file for security vulne
|
|
|
8117
9002
|
new Option("-o, --output <format>", "Output format: table, json, sarif").choices(["table", "json", "sarif"]).default("table")
|
|
8118
9003
|
).addOption(
|
|
8119
9004
|
new Option("-s, --severity <level>", "Minimum severity: critical, high, medium, low").choices(["critical", "high", "medium", "low"]).default("low")
|
|
8120
|
-
).option("--ci", "CI mode: exit code 1 on high/critical findings", false).option("--upload", "Upload results to your ShipSafe dashboard", false).option(
|
|
9005
|
+
).option("--ci", "CI mode: exit code 1 on high/critical findings", false).option("--upload", "Upload results to your ShipSafe dashboard", false).option("--sarif-file <path>", "Also write SARIF output to a file (for CI / GitHub Action)").option("--json-file <path>", "Also write JSON output to a file (for CI / GitHub Action)").option(
|
|
8121
9006
|
"--api-url <url>",
|
|
8122
9007
|
"API URL for ShipSafe server",
|
|
8123
9008
|
validateApiUrl,
|