xploitscan 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,11 @@
1
- # xploitscan
1
+ # XploitScan
2
2
 
3
- AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do.
3
+ [![npm version](https://img.shields.io/npm/v/xploitscan.svg)](https://www.npmjs.com/package/xploitscan)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
4
5
 
5
- Built for solo devs and non-technical founders shipping AI-generated code via Cursor, Lovable, Bolt, Replit, and Claude Code.
6
+ **Security scanner for AI-generated code.** Find vulnerabilities before attackers do.
7
+
8
+ Built for developers shipping code via Cursor, Lovable, Bolt, Replit, and Claude Code. 131 security rules. Plain-English results. Copy-paste fixes.
6
9
 
7
10
  ## Quick Start
8
11
 
@@ -10,29 +13,29 @@ Built for solo devs and non-technical founders shipping AI-generated code via Cu
10
13
  npx xploitscan scan .
11
14
  ```
12
15
 
13
- That's it. No config needed.
16
+ No install, no config, no account required. Your code stays 100% local.
14
17
 
15
18
  ## What It Catches
16
19
 
17
- | Rule | Vulnerability | Severity |
18
- |------|--------------|----------|
19
- | VC001 | Hardcoded API keys & secrets (AWS, Stripe, OpenAI, Supabase, DB URLs) | Critical |
20
- | VC002 | .env files with secrets committed to git | High |
21
- | VC003 | API routes missing authentication | High |
22
- | VC004 | Supabase service_role key in client code / RLS bypass | Critical |
23
- | VC005 | Stripe webhooks without signature verification | Critical |
24
- | VC006 | SQL injection via string interpolation | Critical |
25
- | VC007 | XSS (dangerouslySetInnerHTML, innerHTML, v-html) | High |
26
- | VC008 | Server without rate limiting | Medium |
27
- | VC009 | Wildcard CORS configuration | Medium |
28
- | VC010 | Client-side only authorization checks | High |
29
-
30
- Plus **AI-powered contextual analysis** that catches issues static rules miss.
20
+ 131 rules across 15+ categories:
21
+
22
+ | Category | Examples | Rules |
23
+ |----------|---------|-------|
24
+ | **Secrets** | Hardcoded API keys, .env files, OAuth secrets, Terraform state | 15+ |
25
+ | **Injection** | SQL, XSS, SSRF, command injection, path traversal, XXE, SSTI | 20+ |
26
+ | **Authentication** | Missing auth, weak JWT, insecure password reset, OAuth flaws | 15+ |
27
+ | **Cryptography** | Weak RSA, deprecated TLS, ECB mode, hardcoded IVs | 10+ |
28
+ | **Infrastructure** | Dockerfile, Kubernetes, Terraform, AWS IAM misconfigs | 10+ |
29
+ | **Supply Chain** | Unpinned GitHub Actions, vulnerable dependencies | 5+ |
30
+ | **Information Leakage** | PII in logs, unencrypted DB fields, exposed admin routes | 10+ |
31
+ | **Code Quality** | Console.log in production, empty catch blocks, TODO/FIXME | 10+ |
32
+
33
+ Every finding includes OWASP Top 10 and CWE compliance mappings.
31
34
 
32
35
  ## Installation
33
36
 
34
37
  ```bash
35
- # Run directly (no install)
38
+ # Run directly (recommended — always latest version)
36
39
  npx xploitscan scan .
37
40
 
38
41
  # Or install globally
@@ -44,105 +47,102 @@ xploitscan scan .
44
47
 
45
48
  ```bash
46
49
  # Scan current directory
47
- xploitscan scan .
50
+ npx xploitscan scan .
48
51
 
49
- # Scan a specific directory
50
- xploitscan scan ./my-project
52
+ # Scan a specific folder
53
+ npx xploitscan scan ./src
51
54
 
52
- # Skip AI analysis (faster, no API key needed)
53
- xploitscan scan . --no-ai
55
+ # JSON output (for scripting/CI)
56
+ npx xploitscan scan . --format json
54
57
 
55
- # JSON output (for CI pipelines)
56
- xploitscan scan . --format json
58
+ # SARIF output (for GitHub Security tab)
59
+ npx xploitscan scan . --format sarif
57
60
 
58
- # SARIF output (for GitHub Code Scanning)
59
- xploitscan scan . --format sarif
61
+ # Scan only changed files vs main branch
62
+ npx xploitscan scan . --diff
60
63
 
61
- # Verbose output (show per-scanner results)
62
- xploitscan scan . -v
64
+ # Watch mode re-scan on file changes
65
+ npx xploitscan scan . --watch
63
66
  ```
64
67
 
65
- ## AI-Powered Analysis
68
+ ## Output Formats
66
69
 
67
- Set your Anthropic API key for deeper, contextual vulnerability analysis:
70
+ | Format | Use Case |
71
+ |--------|----------|
72
+ | `text` | Human-readable terminal output (default) |
73
+ | `json` | Machine-readable JSON with all findings |
74
+ | `sarif` | GitHub Security tab integration |
68
75
 
69
- ```bash
70
- export ANTHROPIC_API_KEY=sk-ant-...
71
- xploitscan scan .
72
- ```
73
-
74
- The AI analyzer understands your code in context and explains vulnerabilities in plain English with specific fix instructions.
76
+ ## GitHub Action
75
77
 
76
- ## CI Integration
77
-
78
- ### GitHub Actions
78
+ Add automated scanning to every PR:
79
79
 
80
80
  ```yaml
81
81
  name: Security Scan
82
82
  on: [push, pull_request]
83
83
 
84
84
  jobs:
85
- xploitscan:
85
+ security:
86
86
  runs-on: ubuntu-latest
87
87
  steps:
88
88
  - uses: actions/checkout@v4
89
- - uses: xploitscan/action@v1
90
- with:
91
- anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
92
- ```
93
-
94
- Results appear in the GitHub Security tab.
95
89
 
96
- ### Any CI
90
+ - name: Run XploitScan
91
+ uses: bgage72590/xploitscan@main
92
+ with:
93
+ path: '.'
94
+ format: 'sarif'
95
+ fail-on: 'critical'
97
96
 
98
- ```bash
99
- npx xploitscan scan . --format sarif --no-ai > results.sarif
97
+ - name: Upload SARIF
98
+ if: always()
99
+ uses: github/codeql-action/upload-sarif@v3
100
+ with:
101
+ sarif_file: xploitscan-results.sarif
100
102
  ```
101
103
 
102
- Exit code is 1 when critical or high severity issues are found.
104
+ Findings appear in the GitHub Security tab as code scanning alerts.
103
105
 
104
106
  ## Configuration
105
107
 
106
- Create a `.xploitscanrc.json` in your project root:
108
+ Create a `.xploitscanrc` file in your project root:
107
109
 
108
110
  ```json
109
111
  {
110
- "exclude": ["tests/**", "scripts/**"],
111
- "ai": true,
112
- "severity": "medium",
113
- "disableRules": ["VC008"]
112
+ "rules": {
113
+ "include": ["VC001-VC131"],
114
+ "exclude": ["VC042"]
115
+ },
116
+ "format": "json",
117
+ "fail-on": "high",
118
+ "ignore": ["node_modules", "dist", ".git"]
114
119
  }
115
120
  ```
116
121
 
117
- ## Optional: Deeper Scanning
122
+ ## Web Dashboard
118
123
 
119
- Install these tools for additional detection coverage:
124
+ Scan via the web at [xploitscan.com](https://xploitscan.com):
120
125
 
121
- ```bash
122
- # Semgrep - 2000+ community security rules
123
- pip install semgrep
126
+ - Drag-and-drop file/ZIP upload
127
+ - GitHub URL scanning
128
+ - Scan history and score trends
129
+ - PDF security reports
130
+ - SOC2/ISO27001 compliance mapping
131
+ - Slack and Discord webhook notifications
124
132
 
125
- # Gitleaks - advanced secret detection
126
- brew install gitleaks
127
- ```
128
-
129
- XploitScan automatically uses them if available.
133
+ **Free**: 5 scans/day, 30 core rules. **Pro** ($29/mo): unlimited scans, all 131 rules, and all dashboard features.
130
134
 
131
- ## Auth & Pro Plan
135
+ ## Supported Languages
132
136
 
133
- ```bash
134
- # Log in to sync scan history
135
- xploitscan auth login
137
+ JavaScript, TypeScript, Python, Ruby, Go, Rust, Java, PHP, Swift, Kotlin, C#, Dart, C/C++, and configuration files (Dockerfile, Terraform, Kubernetes, GitHub Actions, .env).
136
138
 
137
- # Check your plan
138
- xploitscan auth whoami
139
-
140
- # Upgrade to Pro ($29/mo) for unlimited scans
141
- xploitscan upgrade
142
- ```
139
+ ## Links
143
140
 
144
- Free plan: 3 scans/day. Pro: unlimited scans, scan history, team features.
141
+ - **Website**: [xploitscan.com](https://xploitscan.com)
142
+ - **Documentation**: [xploitscan.com/docs](https://xploitscan.com/docs)
143
+ - **Changelog**: [xploitscan.com/changelog](https://xploitscan.com/changelog)
144
+ - **Email**: admin@xploitscan.com
145
145
 
146
146
  ## License
147
147
 
148
- MIT
148
+ MIT -- [Cipherline LLC](https://xploitscan.com)
@@ -9,7 +9,7 @@ import {
9
9
  storeToken,
10
10
  syncUser,
11
11
  uploadScanResults
12
- } from "./chunk-47X3TUVR.js";
12
+ } from "./chunk-CBDFSACC.js";
13
13
  export {
14
14
  checkUsage,
15
15
  clearToken,
@@ -21,4 +21,4 @@ export {
21
21
  syncUser,
22
22
  uploadScanResults
23
23
  };
24
- //# sourceMappingURL=api-QTNXZY4O.js.map
24
+ //# sourceMappingURL=api-Z7VNGPT2.js.map
@@ -1,10 +1,4 @@
1
1
  #!/usr/bin/env node
2
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
- }) : x)(function(x) {
5
- if (typeof require !== "undefined") return require.apply(this, arguments);
6
- throw Error('Dynamic require of "' + x + '" is not supported');
7
- });
8
2
 
9
3
  // src/utils/api.ts
10
4
  import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "fs";
@@ -114,7 +108,6 @@ async function getCheckoutUrl() {
114
108
  }
115
109
 
116
110
  export {
117
- __require,
118
111
  getStoredToken,
119
112
  storeToken,
120
113
  clearToken,
@@ -125,4 +118,4 @@ export {
125
118
  syncUser,
126
119
  getCheckoutUrl
127
120
  };
128
- //# sourceMappingURL=chunk-47X3TUVR.js.map
121
+ //# sourceMappingURL=chunk-CBDFSACC.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/utils/api.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nconst CONFIG_DIR = join(homedir(), \".xploitscan\");\nconst TOKEN_FILE = join(CONFIG_DIR, \"token.json\");\n\nconst API_BASE = process.env.XPLOITSCAN_API_URL ?? \"https://api.xploitscan.com\";\n\nif (API_BASE.startsWith(\"http://\") && !API_BASE.includes(\"localhost\") && !API_BASE.includes(\"127.0.0.1\")) {\n console.warn(\"WARNING: API URL is not using HTTPS. This is insecure.\");\n}\n\ninterface TokenData {\n token: string;\n userId: string;\n email: string;\n expiresAt?: number;\n}\n\nexport function getStoredToken(): TokenData | null {\n try {\n if (!existsSync(TOKEN_FILE)) return null;\n const data = JSON.parse(readFileSync(TOKEN_FILE, \"utf-8\"));\n if (data.expiresAt && Date.now() > data.expiresAt) {\n // Token expired\n unlinkSync(TOKEN_FILE);\n return null;\n }\n return data;\n } catch {\n return null;\n }\n}\n\nexport function storeToken(data: TokenData): void {\n mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });\n writeFileSync(TOKEN_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });\n}\n\nexport function clearToken(): void {\n try {\n if (existsSync(TOKEN_FILE)) {\n unlinkSync(TOKEN_FILE);\n }\n } catch {\n // ignore\n }\n}\n\nexport function isAuthenticated(): boolean {\n return getStoredToken() !== null;\n}\n\nasync function apiRequest(\n path: string,\n options: RequestInit = {},\n): Promise<Response> {\n const token = getStoredToken();\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...(options.headers as Record<string, string>),\n };\n\n if (token) {\n headers.Authorization = `Bearer ${token.token}`;\n }\n\n return fetch(`${API_BASE}${path}`, {\n ...options,\n headers,\n });\n}\n\nexport async function checkUsage(): Promise<{\n allowed: boolean;\n plan: string;\n remaining: number;\n limit: number;\n}> {\n const token = getStoredToken();\n if (!token) {\n // Unauthenticated users get limited local scans\n return { allowed: true, plan: \"anonymous\", remaining: -1, limit: -1 };\n }\n\n try {\n const res = await apiRequest(\"/api/usage/check\");\n if (!res.ok) {\n // API error — allow scan to proceed locally\n return { allowed: true, plan: \"unknown\", remaining: -1, limit: -1 };\n }\n return await res.json();\n } catch {\n // Network error — allow local scan\n return { allowed: true, plan: \"offline\", remaining: -1, limit: -1 };\n }\n}\n\nexport async function incrementUsage(): Promise<void> {\n const token = getStoredToken();\n if (!token) return;\n\n try {\n await apiRequest(\"/api/usage/increment\", { method: \"POST\" });\n } catch {\n // Silent fail — don't block scan\n }\n}\n\nexport async function uploadScanResults(result: {\n directory: string;\n filesScanned: number;\n findings: unknown[];\n duration: number;\n}): Promise<void> {\n const token = getStoredToken();\n if (!token) return;\n\n try {\n await apiRequest(\"/api/scans\", {\n method: \"POST\",\n body: JSON.stringify(result),\n });\n } catch {\n // Silent fail\n }\n}\n\nexport async function syncUser(): Promise<{ plan: string; email: string } | null> {\n try {\n const res = await apiRequest(\"/api/users/sync\", { method: \"POST\" });\n if (!res.ok) return null;\n const data = await res.json();\n return data.user;\n } catch {\n return null;\n }\n}\n\nexport async function getCheckoutUrl(): Promise<string | null> {\n try {\n const res = await apiRequest(\"/api/billing/checkout\", { method: \"POST\" });\n if (!res.ok) return null;\n const data = await res.json();\n return data.url;\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/utils/api.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nconst CONFIG_DIR = join(homedir(), \".xploitscan\");\nconst TOKEN_FILE = join(CONFIG_DIR, \"token.json\");\n\nconst API_BASE = process.env.XPLOITSCAN_API_URL ?? \"https://api.xploitscan.com\";\n\nif (API_BASE.startsWith(\"http://\") && !API_BASE.includes(\"localhost\") && !API_BASE.includes(\"127.0.0.1\")) {\n console.warn(\"WARNING: API URL is not using HTTPS. This is insecure.\");\n}\n\ninterface TokenData {\n token: string;\n userId: string;\n email: string;\n expiresAt?: number;\n}\n\nexport function getStoredToken(): TokenData | null {\n try {\n if (!existsSync(TOKEN_FILE)) return null;\n const data = JSON.parse(readFileSync(TOKEN_FILE, \"utf-8\"));\n if (data.expiresAt && Date.now() > data.expiresAt) {\n // Token expired\n unlinkSync(TOKEN_FILE);\n return null;\n }\n return data;\n } catch {\n return null;\n }\n}\n\nexport function storeToken(data: TokenData): void {\n mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });\n writeFileSync(TOKEN_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });\n}\n\nexport function clearToken(): void {\n try {\n if (existsSync(TOKEN_FILE)) {\n unlinkSync(TOKEN_FILE);\n }\n } catch {\n // ignore\n }\n}\n\nexport function isAuthenticated(): boolean {\n return getStoredToken() !== null;\n}\n\nasync function apiRequest(\n path: string,\n options: RequestInit = {},\n): Promise<Response> {\n const token = getStoredToken();\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...(options.headers as Record<string, string>),\n };\n\n if (token) {\n headers.Authorization = `Bearer ${token.token}`;\n }\n\n return fetch(`${API_BASE}${path}`, {\n ...options,\n headers,\n });\n}\n\nexport async function checkUsage(): Promise<{\n allowed: boolean;\n plan: string;\n remaining: number;\n limit: number;\n}> {\n const token = getStoredToken();\n if (!token) {\n // Unauthenticated users get limited local scans\n return { allowed: true, plan: \"anonymous\", remaining: -1, limit: -1 };\n }\n\n try {\n const res = await apiRequest(\"/api/usage/check\");\n if (!res.ok) {\n // API error — allow scan to proceed locally\n return { allowed: true, plan: \"unknown\", remaining: -1, limit: -1 };\n }\n return await res.json();\n } catch {\n // Network error — allow local scan\n return { allowed: true, plan: \"offline\", remaining: -1, limit: -1 };\n }\n}\n\nexport async function incrementUsage(): Promise<void> {\n const token = getStoredToken();\n if (!token) return;\n\n try {\n await apiRequest(\"/api/usage/increment\", { method: \"POST\" });\n } catch {\n // Silent fail — don't block scan\n }\n}\n\nexport async function uploadScanResults(result: {\n directory: string;\n filesScanned: number;\n findings: unknown[];\n duration: number;\n}): Promise<void> {\n const token = getStoredToken();\n if (!token) return;\n\n try {\n await apiRequest(\"/api/scans\", {\n method: \"POST\",\n body: JSON.stringify(result),\n });\n } catch {\n // Silent fail\n }\n}\n\nexport async function syncUser(): Promise<{ plan: string; email: string } | null> {\n try {\n const res = await apiRequest(\"/api/users/sync\", { method: \"POST\" });\n if (!res.ok) return null;\n const data = await res.json();\n return data.user;\n } catch {\n return null;\n }\n}\n\nexport async function getCheckoutUrl(): Promise<string | null> {\n try {\n const res = await apiRequest(\"/api/billing/checkout\", { method: \"POST\" });\n if (!res.ok) return null;\n const data = await res.json();\n return data.url;\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":[]}