opensecurity 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 eliophan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,268 @@
1
+ # OpenSecurity
2
+
3
+ OpenSecurity is an open-source CLI for scanning codebases for security risks, with first-class static analysis for JavaScript/TypeScript and optional AI scanning for broader files.
4
+ It combines:
5
+
6
+ - Static analysis with AST-based taint rules (OWASP-focused).
7
+ - Dependency scanning with CVE lookup (local cache or API).
8
+ - Optional AI-assisted scanning for deeper findings.
9
+
10
+ ## Project Status
11
+
12
+ Active. This repo is maintained and intended for open-source use. Contributions are welcome.
13
+
14
+ ## Scope
15
+
16
+ - Target languages for static analysis: JavaScript and TypeScript.
17
+ - Dependency scanning: npm and PyPI manifests.
18
+ - AI scanning is optional and requires an API key (defaults to scanning all text files).
19
+
20
+ ## Non-Goals
21
+
22
+ - This tool is not a full SAST replacement or compliance scanner.
23
+ - It does not execute or sandbox code.
24
+ - It does not guarantee complete coverage of all vulnerabilities.
25
+
26
+ ## Quick Start
27
+
28
+ ```bash
29
+ npm install
30
+ npm run dev -- scan --dry-run
31
+ ```
32
+
33
+ Build the CLI:
34
+
35
+ ```bash
36
+ npm run build
37
+ ./dist/cli.js scan --dry-run
38
+ ```
39
+
40
+ ## Install
41
+
42
+ From source:
43
+
44
+ ```bash
45
+ npm install
46
+ npm run build
47
+ npm link
48
+ opensecurity scan --dry-run
49
+ ```
50
+
51
+ ## Supported Platforms
52
+
53
+ - Node.js 20+
54
+ - macOS, Linux, Windows
55
+
56
+ ## Features
57
+
58
+ - AST taint engine with configurable sources/sinks/sanitizers.
59
+ - OWASP-aligned default rules (injection, SSRF, path traversal, XSS templates, SQLi).
60
+ - Pattern-based detectors (hardcoded secrets, insecure crypto, unsafe deserialization).
61
+ - Dependency scanning for npm and PyPI (`package.json`, `package-lock.json`, `requirements.txt`).
62
+ - Text, JSON, and SARIF output.
63
+ - Optional AI scan (API key or OAuth flow) with multiple providers.
64
+ - Configurable include/exclude filters and scan scope.
65
+
66
+ ## CLI
67
+
68
+ ### `scan`
69
+
70
+ ```bash
71
+ opensecurity scan [options]
72
+ ```
73
+
74
+ Common options:
75
+
76
+ - `--format <format>`: `text|json|sarif` (default: `text`)
77
+ - `--include <pattern...>` / `--exclude <pattern...>`: override project filters
78
+ - `--rules <path>`: rules JSON override
79
+ - `--cve-cache <path>`: CVE cache JSON
80
+ - `--cve-api-url <url>`: CVE lookup API endpoint
81
+ - `--simulate`: include payload + impact for dependency findings
82
+ - `--provider <provider>`: `openai|anthropic|google|mistral|xai|cohere`
83
+ - `--ai-all-text`: allow AI scan on all text files (non-JS/TS) (default)
84
+ - `--ai-js-only`: limit AI scan to JS/TS only
85
+ - `--path <path>`: scan a specific file or directory
86
+ - `--diff-only`: scan only files changed in git
87
+ - `--diff-base <ref>`: git base ref for diff-only (default: HEAD)
88
+ - `--ai-multi-agent`: split AI scan into worker batches
89
+ - `--ai-batch-size <n>`: files per AI worker batch (default: 25)
90
+ - `--ai-batch-depth <n>`: path depth for AI batching (default: 2)
91
+ - `--ai-cache`: enable AI per-file cache (default)
92
+ - `--no-ai-cache`: disable AI per-file cache
93
+ - `--ai-cache-path <path>`: path to AI cache file
94
+ - `--dependency-only`: only run dependency scan
95
+ - `--no-ai`: disable AI scanning
96
+ - `--dry-run`: list matched files without scanning
97
+ - `--fail-on <severity>`: exit 1 if findings >= severity
98
+ - `--fail-on-high`: exit 1 if findings >= high
99
+ - `--sarif-output <path>`: write SARIF alongside primary output
100
+ - `--concurrency <n>`: parallel scan workers
101
+ - `--max-chars <n>`: max chars per chunk for AI scanning
102
+
103
+ Example commands:
104
+
105
+ ```bash
106
+ opensecurity scan --no-ai
107
+ opensecurity scan --format sarif --sarif-output reports/opensecurity.sarif
108
+ opensecurity scan --provider anthropic --model claude-sonnet-4-20250514
109
+ opensecurity scan --dependency-only --simulate
110
+ ```
111
+
112
+ ### `login`
113
+
114
+ ```bash
115
+ opensecurity login --mode oauth
116
+ opensecurity login --mode api_key
117
+ opensecurity login --mode api_key --provider anthropic
118
+ ```
119
+
120
+ Stores auth config in `~/.config/opensecurity/config.json`.
121
+
122
+ ### `proxy`
123
+
124
+ ```bash
125
+ opensecurity proxy --port 8787
126
+ ```
127
+
128
+ Runs a local OAuth proxy for the OAuth flow.
129
+
130
+ ### `telemetry`
131
+
132
+ ```bash
133
+ opensecurity telemetry on
134
+ opensecurity telemetry off
135
+ ```
136
+
137
+ ## Configuration
138
+
139
+ ### Precedence
140
+
141
+ 1. CLI flags
142
+ 2. Project config (`.opensecurity.json`)
143
+ 3. Global config (`~/.config/opensecurity/config.json`)
144
+ 4. Built-in defaults
145
+
146
+ Project config: `.opensecurity.json`
147
+
148
+ ```json
149
+ {
150
+ "include": ["**/*.ts", "**/*.tsx"],
151
+ "exclude": ["**/*.test.ts"],
152
+ "rulesPath": "rules.json",
153
+ "cveCachePath": "cve-cache.json",
154
+ "cveApiUrl": "https://example.com/osv",
155
+ "dataSensitivity": "medium",
156
+ "maxChars": 4000,
157
+ "concurrency": 2
158
+ }
159
+ ```
160
+
161
+ Global config: `~/.config/opensecurity/config.json`
162
+
163
+ ```json
164
+ {
165
+ "provider": "openai",
166
+ "apiKey": "sk-...",
167
+ "baseUrl": "https://api.openai.com/v1/responses",
168
+ "model": "gpt-4o-mini",
169
+ "apiType": "responses",
170
+ "authMode": "api_key",
171
+ "authProfileId": "codex",
172
+ "oauthProvider": "proxy",
173
+ "providerApiKey": "..."
174
+ }
175
+ ```
176
+
177
+ ## Rules
178
+
179
+ Default rules are in `src/rules/defaultRules.ts`.
180
+ You can override with a JSON file (`--rules` or `rulesPath`).
181
+
182
+ Pattern-based detectors run alongside rules (hardcoded secrets, insecure crypto, unsafe deserialization).
183
+
184
+ Rule schema (simplified):
185
+
186
+ ```json
187
+ {
188
+ "id": "rule-id",
189
+ "name": "Human name",
190
+ "severity": "low|medium|high|critical",
191
+ "owasp": "A03:2021 Injection",
192
+ "sources": [{ "id": "src", "name": "getUserInput", "matcher": { "callee": "getUserInput" } }],
193
+ "sinks": [{ "id": "sink", "name": "eval", "matcher": { "callee": "eval" } }],
194
+ "sanitizers": [{ "id": "san", "name": "escape", "matcher": { "callee": "escape" } }]
195
+ }
196
+ ```
197
+
198
+ ## Output
199
+
200
+ - **Text**: grouped by severity
201
+ - **JSON**: machine-readable, includes `schemaVersion`
202
+ - **SARIF**: for CI and code scanning tools
203
+
204
+ ## Language Support
205
+
206
+ - Static analysis: JavaScript and TypeScript
207
+ - Dependency scanning: npm and PyPI manifests
208
+
209
+ ## Security Notes
210
+
211
+ - Do not commit secrets.
212
+ - AI scanning sends code chunks to the configured API endpoint.
213
+ - Use `--no-ai` if you want purely local scanning.
214
+ - This tool provides best-effort findings and should be validated in your environment.
215
+
216
+ ## Providers
217
+
218
+ Supported providers for AI scanning:
219
+
220
+ - OpenAI (Responses or Chat Completions)
221
+ - Anthropic Messages API
222
+ - Google Gemini API
223
+ - Mistral Chat Completions API
224
+ - xAI Chat Completions API
225
+ - Cohere Chat API
226
+
227
+ API keys can be stored via `opensecurity login --mode api_key --provider <provider>` or set via environment:
228
+
229
+ - `OPENAI_API_KEY`
230
+ - `ANTHROPIC_API_KEY`
231
+ - `GEMINI_API_KEY`
232
+ - `MISTRAL_API_KEY`
233
+ - `XAI_API_KEY`
234
+ - `COHERE_API_KEY`
235
+
236
+ When an API key is available, the model picker will try to fetch a live model list for the provider.
237
+
238
+ ## Contributing
239
+
240
+ - Run `npm test`, `npm run lint`, and `npm run build` before submitting changes.
241
+ - Keep changes focused and add tests for new behavior.
242
+ - Do not add or log real secrets.
243
+ - For large changes, open an issue first to align on scope.
244
+
245
+ See `CONTRIBUTING.md` for full guidelines.
246
+
247
+ ## Security
248
+
249
+ If you discover a vulnerability:
250
+
251
+ - Prefer opening a private **Security Advisory** on GitHub (if enabled), or
252
+ - Open a minimal public issue without sensitive details and request private follow‑up.
253
+
254
+ ## Support
255
+
256
+ For questions or help, open a GitHub issue with clear reproduction steps.
257
+
258
+ ## Development
259
+
260
+ ```bash
261
+ npm install
262
+ npm run dev -- scan --dry-run
263
+ npm test
264
+ ```
265
+
266
+ ## License
267
+
268
+ MIT (see `LICENSE`).
@@ -0,0 +1,20 @@
1
+ import { parse } from "@babel/parser";
2
+ export function parseSource(code, filePath, options = {}) {
3
+ const ast = parse(code, {
4
+ sourceType: options.sourceType ?? "module",
5
+ sourceFilename: filePath,
6
+ allowReturnOutsideFunction: true,
7
+ errorRecovery: true,
8
+ plugins: [
9
+ "typescript",
10
+ "jsx",
11
+ "classProperties",
12
+ "classPrivateProperties",
13
+ "decorators-legacy",
14
+ "dynamicImport",
15
+ "importMeta",
16
+ "topLevelAwait"
17
+ ]
18
+ });
19
+ return { filePath, code, ast };
20
+ }
@@ -0,0 +1,295 @@
1
+ import traverseImport from "@babel/traverse";
2
+ import * as t from "@babel/types";
3
+ export function buildImportGraph(ast, filePath) {
4
+ const traverse = normalizeTraverse(traverseImport);
5
+ const graph = new Map();
6
+ graph.set(filePath, new Set());
7
+ traverse(ast, {
8
+ ImportDeclaration(path) {
9
+ const source = path.node.source.value;
10
+ graph.get(filePath)?.add(source);
11
+ },
12
+ CallExpression(path) {
13
+ const callee = path.node.callee;
14
+ if (!t.isIdentifier(callee) || callee.name !== "require")
15
+ return;
16
+ const arg = path.node.arguments[0];
17
+ if (!t.isStringLiteral(arg))
18
+ return;
19
+ graph.get(filePath)?.add(arg.value);
20
+ }
21
+ });
22
+ return graph;
23
+ }
24
+ export function buildFunctionMap(ast, filePath) {
25
+ const traverse = normalizeTraverse(traverseImport);
26
+ const map = new Map();
27
+ const registerFunction = (name, node, kind) => {
28
+ const loc = node.loc?.start ? { line: node.loc.start.line, column: node.loc.start.column } : undefined;
29
+ const params = node.params.map((param) => (t.isIdentifier(param) ? param.name : "<pattern>"));
30
+ const id = `${filePath}:${name}:${loc?.line ?? 0}`;
31
+ map.set(id, {
32
+ id,
33
+ name,
34
+ file: filePath,
35
+ loc,
36
+ params,
37
+ isAsync: Boolean(node.async),
38
+ kind
39
+ });
40
+ };
41
+ const moduleId = `${filePath}:<module>:0`;
42
+ map.set(moduleId, {
43
+ id: moduleId,
44
+ name: "<module>",
45
+ file: filePath,
46
+ loc: { line: 0, column: 0 },
47
+ params: [],
48
+ isAsync: false,
49
+ kind: "module"
50
+ });
51
+ traverse(ast, {
52
+ FunctionDeclaration(path) {
53
+ const name = path.node.id?.name ?? "<anonymous>";
54
+ registerFunction(name, path.node, "function");
55
+ },
56
+ FunctionExpression(path) {
57
+ const name = inferFunctionName(path) ?? "<anonymous>";
58
+ registerFunction(name, path.node, "function");
59
+ },
60
+ ArrowFunctionExpression(path) {
61
+ const name = inferFunctionName(path) ?? "<anonymous>";
62
+ registerFunction(name, path.node, "arrow");
63
+ },
64
+ ObjectMethod(path) {
65
+ const name = getPropertyName(path.node.key) ?? "<anonymous>";
66
+ registerFunction(name, path.node, "method");
67
+ },
68
+ ClassMethod(path) {
69
+ const name = getPropertyName(path.node.key) ?? "<anonymous>";
70
+ registerFunction(name, path.node, "method");
71
+ }
72
+ });
73
+ return map;
74
+ }
75
+ export function buildCallGraph(ast, filePath, functions) {
76
+ const traverse = normalizeTraverse(traverseImport);
77
+ const graph = new Map();
78
+ const fnStack = [];
79
+ const ensure = (id) => {
80
+ if (!graph.has(id))
81
+ graph.set(id, new Set());
82
+ };
83
+ const moduleId = [...functions.values()].find((f) => f.kind === "module" && f.file === filePath)?.id;
84
+ const rootId = moduleId ?? `${filePath}:<module>:0`;
85
+ const pushFn = (id) => {
86
+ fnStack.push(id);
87
+ ensure(id);
88
+ };
89
+ const popFn = () => {
90
+ fnStack.pop();
91
+ };
92
+ const currentFn = () => fnStack[fnStack.length - 1] ?? rootId;
93
+ traverse(ast, {
94
+ Program: {
95
+ enter() {
96
+ pushFn(rootId);
97
+ },
98
+ exit() {
99
+ popFn();
100
+ }
101
+ },
102
+ FunctionDeclaration: {
103
+ enter(path) {
104
+ const name = path.node.id?.name ?? "<anonymous>";
105
+ const id = findFunctionId(functions, filePath, name, path.node.loc?.start.line);
106
+ pushFn(id ?? `${filePath}:${name}:${path.node.loc?.start.line ?? 0}`);
107
+ },
108
+ exit() {
109
+ popFn();
110
+ }
111
+ },
112
+ FunctionExpression: {
113
+ enter(path) {
114
+ const name = inferFunctionName(path) ?? "<anonymous>";
115
+ const id = findFunctionId(functions, filePath, name, path.node.loc?.start.line);
116
+ pushFn(id ?? `${filePath}:${name}:${path.node.loc?.start.line ?? 0}`);
117
+ },
118
+ exit() {
119
+ popFn();
120
+ }
121
+ },
122
+ ArrowFunctionExpression: {
123
+ enter(path) {
124
+ const name = inferFunctionName(path) ?? "<anonymous>";
125
+ const id = findFunctionId(functions, filePath, name, path.node.loc?.start.line);
126
+ pushFn(id ?? `${filePath}:${name}:${path.node.loc?.start.line ?? 0}`);
127
+ },
128
+ exit() {
129
+ popFn();
130
+ }
131
+ },
132
+ ObjectMethod: {
133
+ enter(path) {
134
+ const name = getPropertyName(path.node.key) ?? "<anonymous>";
135
+ const id = findFunctionId(functions, filePath, name, path.node.loc?.start.line);
136
+ pushFn(id ?? `${filePath}:${name}:${path.node.loc?.start.line ?? 0}`);
137
+ },
138
+ exit() {
139
+ popFn();
140
+ }
141
+ },
142
+ ClassMethod: {
143
+ enter(path) {
144
+ const name = getPropertyName(path.node.key) ?? "<anonymous>";
145
+ const id = findFunctionId(functions, filePath, name, path.node.loc?.start.line);
146
+ pushFn(id ?? `${filePath}:${name}:${path.node.loc?.start.line ?? 0}`);
147
+ },
148
+ exit() {
149
+ popFn();
150
+ }
151
+ },
152
+ CallExpression(path) {
153
+ const caller = currentFn();
154
+ const callee = getCalleeName(path.node);
155
+ if (!callee)
156
+ return;
157
+ ensure(caller);
158
+ graph.get(caller)?.add(callee);
159
+ }
160
+ });
161
+ return graph;
162
+ }
163
+ export function buildDataFlowGraph(ast, filePath) {
164
+ const traverse = normalizeTraverse(traverseImport);
165
+ const nodes = [];
166
+ const edges = [];
167
+ const scopes = [];
168
+ const pushScope = () => scopes.push(new Map());
169
+ const popScope = () => scopes.pop();
170
+ const currentScope = () => scopes[scopes.length - 1];
171
+ const recordDef = (name, node) => {
172
+ const loc = node.loc?.start ? { line: node.loc.start.line, column: node.loc.start.column } : undefined;
173
+ const dataNode = {
174
+ id: `${filePath}:${name}:${loc?.line ?? 0}:${loc?.column ?? 0}`,
175
+ name,
176
+ file: filePath,
177
+ loc
178
+ };
179
+ nodes.push(dataNode);
180
+ currentScope()?.set(name, dataNode);
181
+ };
182
+ const recordUse = (name, node) => {
183
+ const loc = node.loc?.start ? { line: node.loc.start.line, column: node.loc.start.column } : undefined;
184
+ const useNode = {
185
+ id: `${filePath}:${name}:${loc?.line ?? 0}:${loc?.column ?? 0}:use`,
186
+ name,
187
+ file: filePath,
188
+ loc
189
+ };
190
+ nodes.push(useNode);
191
+ const defNode = currentScope()?.get(name);
192
+ if (defNode) {
193
+ edges.push({ from: defNode, to: useNode, kind: "data" });
194
+ }
195
+ };
196
+ traverse(ast, {
197
+ Program: {
198
+ enter() {
199
+ pushScope();
200
+ },
201
+ exit() {
202
+ popScope();
203
+ }
204
+ },
205
+ Function: {
206
+ enter() {
207
+ pushScope();
208
+ },
209
+ exit() {
210
+ popScope();
211
+ }
212
+ },
213
+ VariableDeclarator(path) {
214
+ const id = path.node.id;
215
+ if (t.isIdentifier(id)) {
216
+ recordDef(id.name, id);
217
+ }
218
+ },
219
+ AssignmentExpression(path) {
220
+ const left = path.node.left;
221
+ if (t.isIdentifier(left)) {
222
+ recordDef(left.name, left);
223
+ }
224
+ },
225
+ UpdateExpression(path) {
226
+ const arg = path.node.argument;
227
+ if (t.isIdentifier(arg)) {
228
+ recordUse(arg.name, arg);
229
+ recordDef(arg.name, arg);
230
+ }
231
+ },
232
+ Identifier(path) {
233
+ if (!path.isReferencedIdentifier())
234
+ return;
235
+ recordUse(path.node.name, path.node);
236
+ }
237
+ });
238
+ return { nodes, edges };
239
+ }
240
+ function getCalleeName(node) {
241
+ const callee = node.callee;
242
+ if (t.isIdentifier(callee))
243
+ return callee.name;
244
+ if (t.isMemberExpression(callee))
245
+ return memberExpressionToString(callee);
246
+ return null;
247
+ }
248
+ function memberExpressionToString(node) {
249
+ if (node.computed)
250
+ return null;
251
+ const object = node.object;
252
+ const property = node.property;
253
+ const objectName = t.isIdentifier(object)
254
+ ? object.name
255
+ : t.isMemberExpression(object)
256
+ ? memberExpressionToString(object)
257
+ : null;
258
+ if (!objectName)
259
+ return null;
260
+ if (!t.isIdentifier(property))
261
+ return null;
262
+ return `${objectName}.${property.name}`;
263
+ }
264
+ function inferFunctionName(path) {
265
+ const parent = path.parentPath;
266
+ if (parent?.isVariableDeclarator() && t.isIdentifier(parent.node.id))
267
+ return parent.node.id.name;
268
+ if (parent?.isAssignmentExpression() && t.isIdentifier(parent.node.left))
269
+ return parent.node.left.name;
270
+ if (parent?.isObjectProperty())
271
+ return getPropertyName(parent.node.key);
272
+ return null;
273
+ }
274
+ function getPropertyName(key) {
275
+ if (t.isIdentifier(key))
276
+ return key.name;
277
+ if (t.isStringLiteral(key))
278
+ return key.value;
279
+ return null;
280
+ }
281
+ function findFunctionId(functions, filePath, name, line) {
282
+ for (const info of functions.values()) {
283
+ if (info.file !== filePath)
284
+ continue;
285
+ if (info.name !== name)
286
+ continue;
287
+ if (line && info.loc?.line !== line)
288
+ continue;
289
+ return info.id;
290
+ }
291
+ return null;
292
+ }
293
+ function normalizeTraverse(value) {
294
+ return value.default ?? value;
295
+ }