seamshield 0.0.1 → 0.1.1
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/README.md +124 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1096 -0
- package/package.json +58 -4
- package/rules/secrets.patterns.yaml +28 -0
- package/rules/ss-agent-mcp-inline-credentials.yaml +23 -0
- package/rules/ss-agent-overbroad-permissions.yaml +27 -0
- package/rules/ss-agent-secrets-in-agent-files.yaml +22 -0
- package/rules/ss-auth-admin-route-unprotected.yaml +24 -0
- package/rules/ss-auth-api-route-no-auth.yaml +24 -0
- package/rules/ss-auth-client-only-guard.yaml +24 -0
- package/rules/ss-auth-cors-wildcard-with-credentials.yaml +21 -0
- package/rules/ss-client-firebase-admin-in-client.yaml +23 -0
- package/rules/ss-client-next-public-secret.yaml +33 -0
- package/rules/ss-client-server-secret-env-in-client.yaml +24 -0
- package/rules/ss-client-supabase-service-role-in-client.yaml +24 -0
- package/rules/ss-convex-internal-not-internal.yaml +25 -0
- package/rules/ss-convex-mutation-no-auth.yaml +26 -0
- package/rules/ss-deps-hallucinated-package.yaml +16 -0
- package/rules/ss-deps-known-vuln.yaml +16 -0
- package/rules/ss-deps-no-lockfile.yaml +17 -0
- package/rules/ss-deps-unpinned-spec.yaml +22 -0
- package/rules/ss-firebase-open-rules.yaml +22 -0
- package/rules/ss-secrets-env-file-committed.yaml +21 -0
- package/rules/ss-secrets-generic-credential-assignment.yaml +26 -0
- package/rules/ss-secrets-hardcoded-provider-key.yaml +47 -0
- package/rules/ss-secrets-private-key-file.yaml +22 -0
- package/rules/ss-secrets-supabase-service-role-key.yaml +25 -0
- package/rules/ss-supabase-permissive-policy.yaml +22 -0
- package/rules/ss-supabase-rls-disabled.yaml +22 -0
- package/schemas/finding.schema.json +92 -0
- package/index.js +0 -2
package/package.json
CHANGED
|
@@ -1,8 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "seamshield",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Security scanner for AI-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Security scanner for AI-generated apps: finds the flaws vibecoded projects predictably ship",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"license": "MIT",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
7
|
+
"homepage": "https://github.com/KaraboGerald/SeamShield#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/KaraboGerald/SeamShield.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/KaraboGerald/SeamShield/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"security",
|
|
18
|
+
"scanner",
|
|
19
|
+
"cli",
|
|
20
|
+
"ai",
|
|
21
|
+
"vibecoding",
|
|
22
|
+
"secrets",
|
|
23
|
+
"sast",
|
|
24
|
+
"nextjs",
|
|
25
|
+
"supabase",
|
|
26
|
+
"firebase",
|
|
27
|
+
"claude-code",
|
|
28
|
+
"cursor",
|
|
29
|
+
"osv",
|
|
30
|
+
"sarif"
|
|
31
|
+
],
|
|
32
|
+
"bin": {
|
|
33
|
+
"seamshield": "dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"README.md",
|
|
37
|
+
"dist",
|
|
38
|
+
"rules",
|
|
39
|
+
"schemas"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup src/index.ts --format esm --clean && tsc -p tsconfig.build.json && rm -rf rules schemas && cp -R ../rules/rules ../rules/schemas . && cp ../../README.md README.md",
|
|
46
|
+
"prepack": "pnpm run build",
|
|
47
|
+
"test": "vitest run"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"commander": "^14.0.0",
|
|
51
|
+
"yaml": "^2.5.0",
|
|
52
|
+
"zod": "^4.0.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@seamshield/core": "workspace:*",
|
|
56
|
+
"@seamshield/rules": "workspace:*",
|
|
57
|
+
"@types/node": "^22.0.0",
|
|
58
|
+
"tsup": "^8.3.0",
|
|
59
|
+
"typescript": "^5.7.0",
|
|
60
|
+
"vitest": "^3.0.0"
|
|
61
|
+
}
|
|
8
62
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Shared provider-key patterns consumed by ss/secrets/hardcoded-provider-key
|
|
2
|
+
# via `patterns_from`. Pattern names appear in finding titles, so keep them
|
|
3
|
+
# stable and provider-prefixed.
|
|
4
|
+
patterns:
|
|
5
|
+
- name: anthropic-api-key
|
|
6
|
+
regex: "sk-ant-[A-Za-z0-9_-]{20,}"
|
|
7
|
+
- name: openai-project-key
|
|
8
|
+
regex: "sk-proj-[A-Za-z0-9_-]{20,}"
|
|
9
|
+
- name: openai-legacy-key
|
|
10
|
+
regex: "sk-[A-Za-z0-9]{40,}"
|
|
11
|
+
- name: stripe-live-secret-key
|
|
12
|
+
regex: "sk_live_[A-Za-z0-9]{16,}"
|
|
13
|
+
- name: stripe-restricted-key
|
|
14
|
+
regex: "rk_live_[A-Za-z0-9]{16,}"
|
|
15
|
+
- name: stripe-webhook-secret
|
|
16
|
+
regex: "whsec_[A-Za-z0-9]{24,}"
|
|
17
|
+
- name: google-api-key
|
|
18
|
+
regex: "AIza[0-9A-Za-z_-]{35}"
|
|
19
|
+
- name: github-pat
|
|
20
|
+
regex: "ghp_[A-Za-z0-9]{36}"
|
|
21
|
+
- name: github-fine-grained-pat
|
|
22
|
+
regex: "github_pat_[A-Za-z0-9_]{36,}"
|
|
23
|
+
- name: aws-access-key-id
|
|
24
|
+
regex: "AKIA[0-9A-Z]{16}"
|
|
25
|
+
- name: slack-bot-token
|
|
26
|
+
regex: "xoxb-[0-9A-Za-z-]{20,}"
|
|
27
|
+
- name: convex-deploy-key
|
|
28
|
+
regex: "(?:prod|preview|dev):[a-z0-9-]+\\|[A-Za-z0-9+/=]{20,}"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
id: ss/agent/mcp-inline-credentials
|
|
2
|
+
severity: high
|
|
3
|
+
title: Inline credential in MCP server configuration
|
|
4
|
+
description: >
|
|
5
|
+
An MCP configuration file passes a literal credential value instead of an
|
|
6
|
+
environment variable reference. MCP configs are commonly committed and
|
|
7
|
+
shared between machines, leaking the credential with them.
|
|
8
|
+
framework_ref: AI_AGENT_DROP_IN.md#default-security-stance
|
|
9
|
+
check:
|
|
10
|
+
type: regex
|
|
11
|
+
redact: true
|
|
12
|
+
include:
|
|
13
|
+
basenames: [".mcp.json", "mcp.json", "claude_desktop_config.json"]
|
|
14
|
+
patterns:
|
|
15
|
+
- name: inline-mcp-credential
|
|
16
|
+
regex: "\"[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)[A-Z0-9_]*\"\\s*:\\s*\"(?![$]|\\$\\{)[^\"]{12,}\""
|
|
17
|
+
fix:
|
|
18
|
+
summary: Reference the credential via ${ENV_VAR} and set it in the environment instead.
|
|
19
|
+
agent_prompt: >
|
|
20
|
+
Replace this literal value with an environment-variable reference (for
|
|
21
|
+
example "${GITHUB_TOKEN}") and document the variable in the project
|
|
22
|
+
README or .env.example. If this config file was committed with the real
|
|
23
|
+
value, tell the user to rotate the credential.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
id: ss/agent/overbroad-permissions
|
|
2
|
+
severity: warn
|
|
3
|
+
title: Overbroad AI agent permission grant
|
|
4
|
+
description: >
|
|
5
|
+
An agent settings file grants blanket permissions — wildcard Bash,
|
|
6
|
+
allow-everything, or bypassing the permission system entirely. One
|
|
7
|
+
prompt-injected instruction then runs with full machine access.
|
|
8
|
+
framework_ref: AI_AGENT_DROP_IN.md#default-security-stance
|
|
9
|
+
check:
|
|
10
|
+
type: regex
|
|
11
|
+
include:
|
|
12
|
+
basenames: ["settings.json", "settings.local.json"]
|
|
13
|
+
path_contains: [".claude"]
|
|
14
|
+
patterns:
|
|
15
|
+
- name: wildcard-bash-allow
|
|
16
|
+
regex: "\"Bash\\(\\*"
|
|
17
|
+
- name: bypass-permissions-mode
|
|
18
|
+
regex: "bypassPermissions|dangerously-skip-permissions"
|
|
19
|
+
- name: allow-everything
|
|
20
|
+
regex: "\"allow\"\\s*:\\s*\\[\\s*\"\\*\""
|
|
21
|
+
fix:
|
|
22
|
+
summary: Replace blanket grants with specific allowed command patterns.
|
|
23
|
+
agent_prompt: >
|
|
24
|
+
Replace the wildcard grant with the specific commands the workflow
|
|
25
|
+
actually needs (for example "Bash(npm test:*)", "Bash(git status)").
|
|
26
|
+
Remove bypassPermissions/skip-permissions defaults so destructive
|
|
27
|
+
actions still prompt.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
id: ss/agent/secrets-in-agent-files
|
|
2
|
+
severity: block
|
|
3
|
+
title: Provider API key in an AI agent instruction file
|
|
4
|
+
description: >
|
|
5
|
+
An agent instruction file (CLAUDE.md, AGENTS.md, .cursorrules, *.mdc)
|
|
6
|
+
contains a provider API key. These files are read into every agent
|
|
7
|
+
session, pasted into prompts, and frequently committed — a key here
|
|
8
|
+
leaks on every channel at once.
|
|
9
|
+
framework_ref: AI_AGENT_DROP_IN.md#default-security-stance
|
|
10
|
+
check:
|
|
11
|
+
type: regex
|
|
12
|
+
redact: true
|
|
13
|
+
include:
|
|
14
|
+
basenames: ["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".cursorrules", ".clinerules", "*.mdc"]
|
|
15
|
+
patterns_from: secrets.patterns.yaml
|
|
16
|
+
fix:
|
|
17
|
+
summary: Remove the key from the instruction file and rotate it.
|
|
18
|
+
agent_prompt: >
|
|
19
|
+
Delete this key from the agent instruction file. If the agent needs the
|
|
20
|
+
credential, reference an environment variable name instead and load it
|
|
21
|
+
at runtime. Tell the user to rotate the key — instruction files are
|
|
22
|
+
copied into prompts and logs, so the value is burned.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
id: ss/auth/admin-route-unprotected
|
|
2
|
+
severity: high
|
|
3
|
+
title: Admin route with no recognizable auth check
|
|
4
|
+
description: >
|
|
5
|
+
A page or route under an /admin/ path contains no recognizable
|
|
6
|
+
authentication or authorization marker. If a parent layout or middleware
|
|
7
|
+
guards it, suppress this with a seamshield-ignore comment; otherwise the
|
|
8
|
+
admin surface is open to anyone with the URL.
|
|
9
|
+
framework_ref: AI_AGENT_DROP_IN.md#default-security-stance
|
|
10
|
+
check:
|
|
11
|
+
type: absence
|
|
12
|
+
include:
|
|
13
|
+
path_contains: ["app/admin", "pages/admin"]
|
|
14
|
+
extensions: [".ts", ".tsx", ".js", ".jsx"]
|
|
15
|
+
patterns:
|
|
16
|
+
- name: auth-markers
|
|
17
|
+
regex: "auth|Auth|session|Session|currentUser|getUser|redirect|signIn|login|Login|clerk|Clerk|verif|jwt|JWT|cookie|Cookie|middleware"
|
|
18
|
+
fix:
|
|
19
|
+
summary: Add a server-side auth + role check before rendering admin content.
|
|
20
|
+
agent_prompt: >
|
|
21
|
+
Add a server-side authentication and role check at the top of this
|
|
22
|
+
admin page or its layout — for example, load the session, verify the
|
|
23
|
+
user has an admin role, and redirect to the login page otherwise. A
|
|
24
|
+
client-side check is not sufficient; the check must run on the server.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
id: ss/auth/api-route-no-auth
|
|
2
|
+
severity: warn
|
|
3
|
+
title: API route with no recognizable auth or signature check
|
|
4
|
+
description: >
|
|
5
|
+
An API route handler contains no recognizable authentication,
|
|
6
|
+
authorization, or webhook-signature marker. Review whether this endpoint
|
|
7
|
+
should really be callable by anyone on the internet.
|
|
8
|
+
framework_ref: AI_AGENT_DROP_IN.md#default-security-stance
|
|
9
|
+
check:
|
|
10
|
+
type: absence
|
|
11
|
+
include:
|
|
12
|
+
path_contains: ["/api/"]
|
|
13
|
+
extensions: [".ts", ".js", ".tsx", ".jsx"]
|
|
14
|
+
patterns:
|
|
15
|
+
- name: auth-or-signature-markers
|
|
16
|
+
regex: "auth|Auth|session|Session|currentUser|getUser|clerk|Clerk|jwt|JWT|cookie|Cookie|bearer|Bearer|signature|webhook|svix|x-api-key|apiKey|API_KEY|verif|rate[Ll]imit|public"
|
|
17
|
+
fix:
|
|
18
|
+
summary: Authenticate the caller, or mark the route as intentionally public.
|
|
19
|
+
agent_prompt: >
|
|
20
|
+
Decide whether this API route should be public. If not, validate the
|
|
21
|
+
caller's session or API key at the top of the handler and return 401
|
|
22
|
+
before doing any work. If it is intentionally public (a health check,
|
|
23
|
+
a webhook with signature verification elsewhere), add a short comment
|
|
24
|
+
saying so and a seamshield-ignore for this rule.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
id: ss/auth/client-only-guard
|
|
2
|
+
severity: warn
|
|
3
|
+
title: Auth guard that only runs in the browser
|
|
4
|
+
description: >
|
|
5
|
+
A "use client" component gates content behind a client-side user check.
|
|
6
|
+
Client-side guards are cosmetic — the data behind them must also be
|
|
7
|
+
protected on the server, or anyone can fetch it directly.
|
|
8
|
+
framework_ref: AI_AGENT_DROP_IN.md#default-security-stance
|
|
9
|
+
check:
|
|
10
|
+
type: regex
|
|
11
|
+
include:
|
|
12
|
+
extensions: [".ts", ".tsx", ".js", ".jsx"]
|
|
13
|
+
file_contains: "[\"']use client[\"']"
|
|
14
|
+
patterns:
|
|
15
|
+
- name: client-side-auth-gate
|
|
16
|
+
regex: "if\\s*\\(\\s*!\\s*(?:user|session|currentUser|isAuthenticated|isLoggedIn|loggedIn|signedIn|isSignedIn)\\b"
|
|
17
|
+
fix:
|
|
18
|
+
summary: Keep the UX guard, but enforce the same check server-side where the data is fetched.
|
|
19
|
+
agent_prompt: >
|
|
20
|
+
Keep this client-side check for UX, but verify that every piece of data
|
|
21
|
+
this component renders is also protected server-side — in the route
|
|
22
|
+
handler, server component, or database policy that produces it. If the
|
|
23
|
+
server already enforces it, suppress this finding with a
|
|
24
|
+
seamshield-ignore comment.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
id: ss/auth/cors-wildcard-with-credentials
|
|
2
|
+
severity: high
|
|
3
|
+
title: CORS wildcard origin combined with credentials
|
|
4
|
+
description: >
|
|
5
|
+
This file allows any origin (Access-Control-Allow-Origin set from a
|
|
6
|
+
wildcard) while also enabling Access-Control-Allow-Credentials. That
|
|
7
|
+
combination lets any website act on behalf of your logged-in users.
|
|
8
|
+
framework_ref: AI_AGENT_DROP_IN.md#default-security-stance
|
|
9
|
+
check:
|
|
10
|
+
type: regex
|
|
11
|
+
file_contains: "Allow-Credentials[\"']?\\s*[,:=]\\s*[\"']?true"
|
|
12
|
+
patterns:
|
|
13
|
+
- name: cors-wildcard-origin
|
|
14
|
+
regex: "Allow-Origin[\"']?\\s*[,:=]\\s*[\"']\\*"
|
|
15
|
+
fix:
|
|
16
|
+
summary: Replace the wildcard with an explicit allowlist of trusted origins.
|
|
17
|
+
agent_prompt: >
|
|
18
|
+
Replace the wildcard origin with an explicit allowlist: read the
|
|
19
|
+
request Origin header, compare it against a fixed set of trusted
|
|
20
|
+
origins, and echo it back only on match. Keep Allow-Credentials only if
|
|
21
|
+
cookies or auth headers are genuinely needed cross-origin.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
id: ss/client/firebase-admin-in-client
|
|
2
|
+
severity: block
|
|
3
|
+
title: firebase-admin imported in a client component
|
|
4
|
+
description: >
|
|
5
|
+
firebase-admin is the server-side SDK that bypasses all Firebase security
|
|
6
|
+
rules. Importing it in a "use client" file either breaks the build or, far
|
|
7
|
+
worse, bundles admin credentials into the browser bundle.
|
|
8
|
+
framework_ref: AI_AGENT_DROP_IN.md#framework-compromise-rule
|
|
9
|
+
check:
|
|
10
|
+
type: regex
|
|
11
|
+
include:
|
|
12
|
+
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"]
|
|
13
|
+
file_contains: "[\"']use client[\"']"
|
|
14
|
+
patterns:
|
|
15
|
+
- name: firebase-admin-import
|
|
16
|
+
regex: "(?:from\\s+[\"']firebase-admin|require\\([\"']firebase-admin)"
|
|
17
|
+
fix:
|
|
18
|
+
summary: Move firebase-admin usage into server-only code; clients use the regular firebase SDK.
|
|
19
|
+
agent_prompt: >
|
|
20
|
+
Remove the firebase-admin import from this client component. Move the
|
|
21
|
+
admin operation into a server route handler or server action, expose
|
|
22
|
+
only the minimal result to the client, and use the regular `firebase`
|
|
23
|
+
client SDK (gated by security rules) for client-side reads.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
id: ss/client/next-public-secret
|
|
2
|
+
severity: block
|
|
3
|
+
title: Secret exposed through a NEXT_PUBLIC_ variable
|
|
4
|
+
description: >
|
|
5
|
+
Next.js inlines every NEXT_PUBLIC_ variable into the client JavaScript
|
|
6
|
+
bundle at build time. A variable named NEXT_PUBLIC_*SECRET*, *SERVICE_ROLE*,
|
|
7
|
+
*PRIVATE*, *PASSWORD*, or *ADMIN* ships that value to every browser that
|
|
8
|
+
loads the app.
|
|
9
|
+
framework_ref: AI_AGENT_DROP_IN.md#framework-compromise-rule
|
|
10
|
+
check:
|
|
11
|
+
type: regex
|
|
12
|
+
include:
|
|
13
|
+
extensions:
|
|
14
|
+
- ".ts"
|
|
15
|
+
- ".tsx"
|
|
16
|
+
- ".js"
|
|
17
|
+
- ".jsx"
|
|
18
|
+
- ".mjs"
|
|
19
|
+
- ".cjs"
|
|
20
|
+
basenames:
|
|
21
|
+
- ".env*"
|
|
22
|
+
patterns:
|
|
23
|
+
- name: next-public-secret-name
|
|
24
|
+
regex: "NEXT_PUBLIC_[A-Z0-9_]*(?:SECRET|SERVICE_ROLE|PRIVATE|PASSWORD|ADMIN)[A-Z0-9_]*"
|
|
25
|
+
fix:
|
|
26
|
+
summary: Rename the variable to drop the NEXT_PUBLIC_ prefix and read it only on the server.
|
|
27
|
+
agent_prompt: >
|
|
28
|
+
Remove the NEXT_PUBLIC_ prefix from this variable so Next.js stops
|
|
29
|
+
inlining it into the client bundle. Move every read of it into
|
|
30
|
+
server-only code (a route handler, server action, or server component)
|
|
31
|
+
and pass only derived, non-secret data to client components. If the app
|
|
32
|
+
was already deployed with this variable, tell the user to rotate the
|
|
33
|
+
underlying credential.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
id: ss/client/server-secret-env-in-client
|
|
2
|
+
severity: high
|
|
3
|
+
title: Server secret environment variable read in a client component
|
|
4
|
+
description: >
|
|
5
|
+
A "use client" file reads a secret-named environment variable. In the
|
|
6
|
+
browser bundle that read resolves to undefined at best — and if the
|
|
7
|
+
bundler inlines it, the secret ships to every visitor.
|
|
8
|
+
framework_ref: AI_AGENT_DROP_IN.md#framework-compromise-rule
|
|
9
|
+
check:
|
|
10
|
+
type: regex
|
|
11
|
+
include:
|
|
12
|
+
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"]
|
|
13
|
+
file_contains: "[\"']use client[\"']"
|
|
14
|
+
patterns:
|
|
15
|
+
- name: server-secret-env-read
|
|
16
|
+
regex: "process\\.env\\.(?!NEXT_PUBLIC_)[A-Z0-9_]*(?:SECRET|PRIVATE|PASSWORD|SERVICE_ROLE|API_KEY|TOKEN)[A-Z0-9_]*"
|
|
17
|
+
fix:
|
|
18
|
+
summary: Move the env read into server code and pass only derived, non-secret data to the client.
|
|
19
|
+
agent_prompt: >
|
|
20
|
+
Move this environment variable read out of the client component into a
|
|
21
|
+
server component, route handler, or server action. Pass only the
|
|
22
|
+
derived, non-secret result down as props. Never rename the variable to
|
|
23
|
+
NEXT_PUBLIC_ to "fix" the undefined value — that ships the secret to
|
|
24
|
+
the browser.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
id: ss/client/supabase-service-role-in-client
|
|
2
|
+
severity: block
|
|
3
|
+
title: Supabase service-role key referenced in a client component
|
|
4
|
+
description: >
|
|
5
|
+
A "use client" file references the Supabase service-role key. The
|
|
6
|
+
service-role key bypasses Row Level Security; any reference in
|
|
7
|
+
client-bundled code risks shipping full database access to every browser.
|
|
8
|
+
framework_ref: AI_AGENT_DROP_IN.md#framework-compromise-rule
|
|
9
|
+
check:
|
|
10
|
+
type: regex
|
|
11
|
+
include:
|
|
12
|
+
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"]
|
|
13
|
+
file_contains: "[\"']use client[\"']"
|
|
14
|
+
patterns:
|
|
15
|
+
- name: service-role-reference
|
|
16
|
+
regex: "SUPABASE_SERVICE_ROLE"
|
|
17
|
+
fix:
|
|
18
|
+
summary: Use the anon key plus RLS in client code; keep the service-role key server-side.
|
|
19
|
+
agent_prompt: >
|
|
20
|
+
Replace this service-role usage with the public anon key and rely on
|
|
21
|
+
Row Level Security policies for authorization. Any operation that truly
|
|
22
|
+
needs service-role privileges must move to a server route handler or
|
|
23
|
+
edge function. If this code ever shipped, tell the user to rotate the
|
|
24
|
+
service-role key.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
id: ss/convex/internal-not-internal
|
|
2
|
+
severity: warn
|
|
3
|
+
title: Function named internal* but exported as public
|
|
4
|
+
description: >
|
|
5
|
+
A Convex function whose name starts with "internal" is declared with the
|
|
6
|
+
public mutation/query/action constructor. The name says private, the
|
|
7
|
+
declaration says public — anyone can call it.
|
|
8
|
+
framework_ref: AI_AGENT_DROP_IN.md#default-security-stance
|
|
9
|
+
check:
|
|
10
|
+
type: regex
|
|
11
|
+
include:
|
|
12
|
+
path_contains: ["convex/"]
|
|
13
|
+
extensions: [".ts", ".js"]
|
|
14
|
+
exclude:
|
|
15
|
+
dirs: ["_generated"]
|
|
16
|
+
patterns:
|
|
17
|
+
- name: internal-named-public-fn
|
|
18
|
+
regex: "const\\s+internal[A-Z]\\w*\\s*=\\s*(?:mutation|query|action)\\s*\\("
|
|
19
|
+
fix:
|
|
20
|
+
summary: Declare it with internalMutation/internalQuery/internalAction instead.
|
|
21
|
+
agent_prompt: >
|
|
22
|
+
Change this declaration to the internal variant (internalMutation,
|
|
23
|
+
internalQuery, or internalAction from ./_generated/server) so it can
|
|
24
|
+
only be invoked by other Convex functions, and update any client-side
|
|
25
|
+
callers — they should not exist if the name is accurate.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
id: ss/convex/mutation-no-auth
|
|
2
|
+
severity: warn
|
|
3
|
+
title: Public Convex mutation with no auth check
|
|
4
|
+
description: >
|
|
5
|
+
A public Convex mutation never consults ctx.auth. Public mutations are
|
|
6
|
+
callable by anyone who can reach the deployment URL — review whether this
|
|
7
|
+
one should verify the caller first.
|
|
8
|
+
framework_ref: AI_AGENT_DROP_IN.md#default-security-stance
|
|
9
|
+
check:
|
|
10
|
+
type: absence
|
|
11
|
+
include:
|
|
12
|
+
path_contains: ["convex/"]
|
|
13
|
+
extensions: [".ts", ".js"]
|
|
14
|
+
exclude:
|
|
15
|
+
dirs: ["_generated"]
|
|
16
|
+
file_contains: "(?<![A-Za-z])mutation\\s*\\("
|
|
17
|
+
patterns:
|
|
18
|
+
- name: convex-auth-markers
|
|
19
|
+
regex: "ctx\\.auth|getAuthUserId|getUserIdentity|requireAuth|internalMutation"
|
|
20
|
+
fix:
|
|
21
|
+
summary: Verify the caller with ctx.auth at the top of the mutation, or make it internal.
|
|
22
|
+
agent_prompt: >
|
|
23
|
+
Add an auth check at the top of this mutation — for example `const
|
|
24
|
+
identity = await ctx.auth.getUserIdentity(); if (!identity) throw new
|
|
25
|
+
Error("Unauthorized");` — or convert it to internalMutation if it
|
|
26
|
+
should only ever be called from other server functions.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
id: ss/deps/hallucinated-package
|
|
2
|
+
severity: block
|
|
3
|
+
title: Dependency does not exist on npm
|
|
4
|
+
description: >
|
|
5
|
+
AI agents sometimes invent package names. Installing a newly created or
|
|
6
|
+
squatted package with that name later can execute attacker-controlled code.
|
|
7
|
+
framework_ref: AI_AGENT_DROP_IN.md#supply-chain-stance
|
|
8
|
+
check:
|
|
9
|
+
type: builtin
|
|
10
|
+
builtin: hallucinated-package
|
|
11
|
+
fix:
|
|
12
|
+
summary: Replace the dependency with a real maintained package or remove it.
|
|
13
|
+
agent_prompt: >
|
|
14
|
+
Verify the intended package name against npm and the project docs. Replace
|
|
15
|
+
this dependency with the maintained package that provides the needed API,
|
|
16
|
+
update imports, reinstall, and commit the updated lockfile.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
id: ss/deps/known-vuln
|
|
2
|
+
severity: high
|
|
3
|
+
title: Dependency version has known vulnerabilities
|
|
4
|
+
description: >
|
|
5
|
+
The pinned dependency version is reported by OSV for the npm ecosystem.
|
|
6
|
+
Known exploited vulnerability indicators are escalated to block severity.
|
|
7
|
+
framework_ref: seamshield-final-framework/adoption-contract/.seamshield/release-gates.yaml
|
|
8
|
+
check:
|
|
9
|
+
type: builtin
|
|
10
|
+
builtin: known-vuln
|
|
11
|
+
fix:
|
|
12
|
+
summary: Upgrade the dependency to a non-vulnerable version and refresh the lockfile.
|
|
13
|
+
agent_prompt: >
|
|
14
|
+
Upgrade this dependency to the nearest non-vulnerable version that satisfies
|
|
15
|
+
the project, update the lockfile, run the affected tests, and document any
|
|
16
|
+
breaking API changes.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
id: ss/deps/no-lockfile
|
|
2
|
+
severity: warn
|
|
3
|
+
title: package.json with no lockfile
|
|
4
|
+
description: >
|
|
5
|
+
No lockfile sits next to package.json, so every install re-resolves
|
|
6
|
+
dependency versions. Builds are not reproducible, and a hijacked patch
|
|
7
|
+
release of any dependency lands silently on the next install.
|
|
8
|
+
framework_ref: AI_AGENT_DROP_IN.md#supply-chain-stance
|
|
9
|
+
check:
|
|
10
|
+
type: builtin
|
|
11
|
+
builtin: no-lockfile
|
|
12
|
+
fix:
|
|
13
|
+
summary: Commit a lockfile (pnpm-lock.yaml, package-lock.json, or yarn.lock).
|
|
14
|
+
agent_prompt: >
|
|
15
|
+
Run the project's package manager install once (npm install / pnpm
|
|
16
|
+
install / yarn) and commit the generated lockfile. Make sure .gitignore
|
|
17
|
+
does not exclude it.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
id: ss/deps/unpinned-spec
|
|
2
|
+
severity: info
|
|
3
|
+
title: Dependency pinned to "latest" or "*"
|
|
4
|
+
description: >
|
|
5
|
+
A dependency version of "latest" or "*" resolves to whatever the
|
|
6
|
+
registry serves at install time, making builds unreproducible and
|
|
7
|
+
auto-adopting compromised releases.
|
|
8
|
+
framework_ref: AI_AGENT_DROP_IN.md#supply-chain-stance
|
|
9
|
+
check:
|
|
10
|
+
type: regex
|
|
11
|
+
include:
|
|
12
|
+
basenames: ["package.json"]
|
|
13
|
+
patterns:
|
|
14
|
+
- name: latest-or-star-spec
|
|
15
|
+
regex: "\"\\s*:\\s*\"(?:\\*|latest)\""
|
|
16
|
+
fix:
|
|
17
|
+
summary: Pin the dependency to a specific version or range.
|
|
18
|
+
agent_prompt: >
|
|
19
|
+
Replace the "latest" or "*" specifier with the currently installed
|
|
20
|
+
version (check the lockfile or node_modules/<pkg>/package.json) using a
|
|
21
|
+
caret range, for example "^2.4.1", then reinstall to update the
|
|
22
|
+
lockfile.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
id: ss/firebase/open-rules
|
|
2
|
+
severity: block
|
|
3
|
+
title: Firebase security rules allow unrestricted access
|
|
4
|
+
description: >
|
|
5
|
+
A Firebase rules file grants read or write with "if true". Every
|
|
6
|
+
document or object it covers is readable or writable by anyone on the
|
|
7
|
+
internet, no authentication required.
|
|
8
|
+
framework_ref: AI_AGENT_DROP_IN.md#default-security-stance
|
|
9
|
+
check:
|
|
10
|
+
type: regex
|
|
11
|
+
include:
|
|
12
|
+
basenames: ["*.rules"]
|
|
13
|
+
patterns:
|
|
14
|
+
- name: allow-if-true
|
|
15
|
+
regex: "allow\\s+[a-z, ]+:\\s*if\\s+true"
|
|
16
|
+
fix:
|
|
17
|
+
summary: Require auth and ownership in the rule condition.
|
|
18
|
+
agent_prompt: >
|
|
19
|
+
Replace `if true` with a real condition — at minimum `if request.auth
|
|
20
|
+
!= null`, and for user data `if request.auth.uid == resource.data.
|
|
21
|
+
ownerId` (or the equivalent path check). Deploy the updated rules and
|
|
22
|
+
verify access with the Firebase rules simulator.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
id: ss/secrets/env-file-committed
|
|
2
|
+
severity: block
|
|
3
|
+
title: .env file is committed or not git-ignored
|
|
4
|
+
description: >
|
|
5
|
+
Dotenv files hold live credentials. If a .env file is tracked by git, or
|
|
6
|
+
sits untracked without a .gitignore entry, the next `git add .` ships every
|
|
7
|
+
secret in it to the remote — the single most common leak in AI-generated
|
|
8
|
+
repos.
|
|
9
|
+
framework_ref: AI_AGENT_DROP_IN.md#default-security-stance
|
|
10
|
+
check:
|
|
11
|
+
type: builtin
|
|
12
|
+
builtin: env-file-committed
|
|
13
|
+
fix:
|
|
14
|
+
summary: Git-ignore the .env file, untrack it, and rotate every credential it contains.
|
|
15
|
+
agent_prompt: >
|
|
16
|
+
Add `.env` and `.env.*` (with an exception for `.env.example`) to
|
|
17
|
+
.gitignore. For each dotenv file already tracked by git, run
|
|
18
|
+
`git rm --cached <file>` so it stays on disk but leaves the index. Create
|
|
19
|
+
a `.env.example` listing variable names with empty values. Finally, tell
|
|
20
|
+
the user every credential in the file must be rotated at its provider,
|
|
21
|
+
because git history still contains the old values.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
id: ss/secrets/generic-credential-assignment
|
|
2
|
+
severity: high
|
|
3
|
+
title: Credential-looking literal assigned in source
|
|
4
|
+
description: >
|
|
5
|
+
A variable or property whose name says secret, token, password, or apikey
|
|
6
|
+
is assigned a long literal value in source code. Even when the value is
|
|
7
|
+
not a recognized provider key, committing credentials to source is unsafe.
|
|
8
|
+
framework_ref: AI_AGENT_DROP_IN.md#default-security-stance
|
|
9
|
+
check:
|
|
10
|
+
type: regex
|
|
11
|
+
redact: true
|
|
12
|
+
include:
|
|
13
|
+
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".rb", ".go", ".yaml", ".yml", ".toml"]
|
|
14
|
+
exclude:
|
|
15
|
+
basenames: [".env*", "package-lock.json", "pnpm-lock.yaml", "yarn.lock", "*.min.js", "*.test.ts", "*.test.js", "*.spec.ts", "*.spec.js"]
|
|
16
|
+
dirs: ["test", "tests", "__tests__", "fixtures", "__fixtures__", "__mocks__"]
|
|
17
|
+
patterns:
|
|
18
|
+
- name: generic-credential
|
|
19
|
+
regex: "(?:[Ss][Ee][Cc][Rr][Ee][Tt]|[Tt][Oo][Kk][Ee][Nn]|[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]|[Aa][Pp][Ii][_-]?[Kk][Ee][Yy])[A-Za-z0-9_]*[\"']?\\s*[:=]\\s*[\"'][A-Za-z0-9+/_-]{32,}[\"']"
|
|
20
|
+
fix:
|
|
21
|
+
summary: Move the value into an environment variable and rotate it if it was real.
|
|
22
|
+
agent_prompt: >
|
|
23
|
+
Replace this literal with a read from a server-side environment
|
|
24
|
+
variable, add the variable name to .env.example with a placeholder
|
|
25
|
+
value, and ask the user whether the committed value was a real
|
|
26
|
+
credential — if so they must rotate it.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
id: ss/secrets/hardcoded-provider-key
|
|
2
|
+
severity: block
|
|
3
|
+
title: Hardcoded provider API key in source
|
|
4
|
+
description: >
|
|
5
|
+
A live API key (Anthropic, OpenAI, Stripe, Google, GitHub, AWS, Slack,
|
|
6
|
+
Convex) is written directly in source code. Anyone with repo read access —
|
|
7
|
+
or anyone who fetches the client bundle — can spend on your account.
|
|
8
|
+
framework_ref: AI_AGENT_DROP_IN.md#default-security-stance
|
|
9
|
+
check:
|
|
10
|
+
type: regex
|
|
11
|
+
patterns_from: secrets.patterns.yaml
|
|
12
|
+
redact: true
|
|
13
|
+
include:
|
|
14
|
+
extensions:
|
|
15
|
+
- ".ts"
|
|
16
|
+
- ".tsx"
|
|
17
|
+
- ".js"
|
|
18
|
+
- ".jsx"
|
|
19
|
+
- ".mjs"
|
|
20
|
+
- ".cjs"
|
|
21
|
+
- ".json"
|
|
22
|
+
- ".yaml"
|
|
23
|
+
- ".yml"
|
|
24
|
+
- ".toml"
|
|
25
|
+
- ".py"
|
|
26
|
+
- ".rb"
|
|
27
|
+
- ".go"
|
|
28
|
+
- ".html"
|
|
29
|
+
- ".svelte"
|
|
30
|
+
- ".vue"
|
|
31
|
+
- ".astro"
|
|
32
|
+
exclude:
|
|
33
|
+
basenames:
|
|
34
|
+
- ".env*"
|
|
35
|
+
- "package-lock.json"
|
|
36
|
+
- "pnpm-lock.yaml"
|
|
37
|
+
- "yarn.lock"
|
|
38
|
+
- "bun.lockb"
|
|
39
|
+
- "*.min.js"
|
|
40
|
+
fix:
|
|
41
|
+
summary: Move the key into an environment variable and rotate it at the provider.
|
|
42
|
+
agent_prompt: >
|
|
43
|
+
Replace the hardcoded key with a server-side environment variable read
|
|
44
|
+
(for example `process.env.PROVIDER_API_KEY`), add the variable name to
|
|
45
|
+
`.env.example`, and make sure the file that reads it only runs on the
|
|
46
|
+
server. Then tell the user to rotate this key at the provider dashboard —
|
|
47
|
+
treat the committed value as burned.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
id: ss/secrets/private-key-file
|
|
2
|
+
severity: block
|
|
3
|
+
title: Private key material committed to the repo
|
|
4
|
+
description: >
|
|
5
|
+
A PEM-encoded private key (RSA, EC, OpenSSH, or PKCS#8) is present in the
|
|
6
|
+
repository. Private keys in git history stay compromised forever, even
|
|
7
|
+
after deletion.
|
|
8
|
+
framework_ref: AI_AGENT_DROP_IN.md#default-security-stance
|
|
9
|
+
check:
|
|
10
|
+
type: regex
|
|
11
|
+
exclude:
|
|
12
|
+
basenames: ["*.pub"]
|
|
13
|
+
patterns:
|
|
14
|
+
- name: pem-private-key
|
|
15
|
+
regex: "-----BEGIN (?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY-----"
|
|
16
|
+
fix:
|
|
17
|
+
summary: Remove the key file, gitignore it, and rotate the key pair.
|
|
18
|
+
agent_prompt: >
|
|
19
|
+
Remove this private key file from the repository, add its path (or
|
|
20
|
+
*.pem / *.key) to .gitignore, and tell the user to rotate the key pair
|
|
21
|
+
everywhere it is used — git history preserves the old key, so deletion
|
|
22
|
+
alone does not make it safe.
|