opensecurity 0.1.0 → 0.2.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.
Files changed (42) hide show
  1. package/README.md +156 -30
  2. package/assets/grammars/README.md +9 -0
  3. package/assets/grammars/tree-sitter-c-sharp.wasm +0 -0
  4. package/assets/grammars/tree-sitter-c.wasm +0 -0
  5. package/assets/grammars/tree-sitter-cpp.wasm +0 -0
  6. package/assets/grammars/tree-sitter-go.wasm +0 -0
  7. package/assets/grammars/tree-sitter-java.wasm +0 -0
  8. package/assets/grammars/tree-sitter-kotlin.wasm +0 -0
  9. package/assets/grammars/tree-sitter-php.wasm +0 -0
  10. package/assets/grammars/tree-sitter-python.wasm +0 -0
  11. package/assets/grammars/tree-sitter-ruby.wasm +0 -0
  12. package/assets/grammars/tree-sitter-rust.wasm +0 -0
  13. package/assets/grammars/tree-sitter-swift.wasm +0 -0
  14. package/dist/adapters/bandit.js +41 -0
  15. package/dist/adapters/brakeman.js +41 -0
  16. package/dist/adapters/gosec.js +49 -0
  17. package/dist/adapters/languages.js +29 -0
  18. package/dist/adapters/runner.js +46 -0
  19. package/dist/adapters/semgrep.js +59 -0
  20. package/dist/adapters/types.js +1 -0
  21. package/dist/adapters/utils.js +52 -0
  22. package/dist/analysis/infraPatterns.js +196 -0
  23. package/dist/analysis/universalPatterns.js +56 -0
  24. package/dist/cli.js +15 -1
  25. package/dist/config.js +2 -1
  26. package/dist/native/languages.js +211 -0
  27. package/dist/native/loader.js +61 -0
  28. package/dist/native/rules.js +14 -0
  29. package/dist/native/taint.js +225 -0
  30. package/dist/scan.js +207 -0
  31. package/package.json +46 -2
  32. package/rules/taint/c.json +47 -0
  33. package/rules/taint/cpp.json +47 -0
  34. package/rules/taint/csharp.json +99 -0
  35. package/rules/taint/go.json +86 -0
  36. package/rules/taint/java.json +101 -0
  37. package/rules/taint/kotlin.json +86 -0
  38. package/rules/taint/php.json +100 -0
  39. package/rules/taint/python.json +108 -0
  40. package/rules/taint/ruby.json +101 -0
  41. package/rules/taint/rust.json +86 -0
  42. package/rules/taint/swift.json +86 -0
@@ -0,0 +1,225 @@
1
+ function getNodeText(node, source) {
2
+ return source.slice(node.startIndex, node.endIndex);
3
+ }
4
+ function normalizeTraverseNode(node) {
5
+ return node;
6
+ }
7
+ function matchesCallee(name, matcher) {
8
+ if (matcher.callee) {
9
+ const match = matcher.callee;
10
+ if (Array.isArray(match) && match.includes(name))
11
+ return true;
12
+ if (match === name)
13
+ return true;
14
+ }
15
+ if (matcher.calleePrefix) {
16
+ const match = matcher.calleePrefix;
17
+ if (Array.isArray(match) && match.some((prefix) => name.startsWith(prefix)))
18
+ return true;
19
+ if (typeof match === "string" && name.startsWith(match))
20
+ return true;
21
+ }
22
+ if (matcher.calleePattern) {
23
+ const match = matcher.calleePattern;
24
+ const patterns = Array.isArray(match) ? match : [match];
25
+ return patterns.some((pattern) => matchGlob(name, pattern));
26
+ }
27
+ return false;
28
+ }
29
+ function matchGlob(value, pattern) {
30
+ if (pattern === value)
31
+ return true;
32
+ try {
33
+ const picomatch = require("picomatch");
34
+ const isMatch = picomatch(pattern, { nocase: false, dot: true });
35
+ return isMatch(value);
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ function getNodeName(node, lang, source) {
42
+ if (lang.identifierNodes.includes(node.type)) {
43
+ return getNodeText(node, source);
44
+ }
45
+ if (lang.memberNodes.includes(node.type)) {
46
+ const objectNode = pickField(node, lang.memberObjectFields);
47
+ const propNode = pickField(node, lang.memberPropertyFields);
48
+ const objectName = objectNode ? getNodeName(objectNode, lang, source) ?? getNodeText(objectNode, source) : null;
49
+ const propName = propNode ? getNodeName(propNode, lang, source) ?? getNodeText(propNode, source) : null;
50
+ if (objectName && propName)
51
+ return `${objectName}.${propName}`;
52
+ if (propName)
53
+ return propName;
54
+ }
55
+ return null;
56
+ }
57
+ function pickField(node, fields) {
58
+ for (const field of fields) {
59
+ const child = node.childForFieldName?.(field);
60
+ if (child)
61
+ return normalizeTraverseNode(child);
62
+ }
63
+ return null;
64
+ }
65
+ function getCallName(node, lang, source) {
66
+ const callee = pickField(node, lang.callCalleeFields) ??
67
+ (node.namedChildren?.[0] ? normalizeTraverseNode(node.namedChildren[0]) : null);
68
+ if (!callee)
69
+ return null;
70
+ return getNodeName(callee, lang, source) ?? getNodeText(callee, source);
71
+ }
72
+ function getCallArguments(node, lang) {
73
+ const argNode = pickField(node, lang.callArgumentFields);
74
+ if (argNode?.namedChildren?.length) {
75
+ return argNode.namedChildren.map(normalizeTraverseNode);
76
+ }
77
+ return [];
78
+ }
79
+ function getAssignmentSides(node, lang) {
80
+ const left = pickField(node, lang.assignmentLeftFields);
81
+ const right = pickField(node, lang.assignmentRightFields);
82
+ return { left: left ? normalizeTraverseNode(left) : null, right: right ? normalizeTraverseNode(right) : null };
83
+ }
84
+ function isStringNode(node, lang) {
85
+ return lang.stringNodes.includes(node.type);
86
+ }
87
+ function walk(node, fn) {
88
+ fn(node);
89
+ const children = node.namedChildren ?? [];
90
+ for (const child of children) {
91
+ walk(normalizeTraverseNode(child), fn);
92
+ }
93
+ }
94
+ function findIdentifiers(node, lang, source) {
95
+ const names = [];
96
+ walk(node, (n) => {
97
+ const name = getNodeName(n, lang, source);
98
+ if (name && lang.identifierNodes.includes(n.type))
99
+ names.push(name);
100
+ });
101
+ return names;
102
+ }
103
+ export function runNativeTaint(tree, source, lang, ruleSet, filePath) {
104
+ const root = normalizeTraverseNode(tree.rootNode ?? tree);
105
+ const findings = [];
106
+ const taintedVarsStack = [new Set()];
107
+ const taintedExpressions = new WeakSet();
108
+ const pushScope = () => taintedVarsStack.push(new Set());
109
+ const popScope = () => taintedVarsStack.pop();
110
+ const currentScope = () => taintedVarsStack[taintedVarsStack.length - 1];
111
+ const taint = (name) => currentScope()?.add(name);
112
+ const untaint = (name) => currentScope()?.delete(name);
113
+ const isTainted = (name) => currentScope()?.has(name) ?? false;
114
+ const valueIsTainted = (node, rule) => {
115
+ if (taintedExpressions.has(node))
116
+ return true;
117
+ const nodeName = getNodeName(node, lang, source);
118
+ if (nodeName && rule.sources?.some((src) => matchesCallee(nodeName, src.matcher))) {
119
+ return true;
120
+ }
121
+ if (lang.identifierNodes.includes(node.type) && nodeName) {
122
+ return isTainted(nodeName);
123
+ }
124
+ if (lang.memberNodes.includes(node.type) && nodeName) {
125
+ return isTainted(nodeName);
126
+ }
127
+ if (lang.callNodes.includes(node.type)) {
128
+ const calleeName = getCallName(node, lang, source);
129
+ if (calleeName) {
130
+ if (rule.sanitizers?.some((san) => matchesCallee(calleeName, san.matcher)))
131
+ return false;
132
+ if (rule.sources?.some((src) => matchesCallee(calleeName, src.matcher)))
133
+ return true;
134
+ }
135
+ }
136
+ const children = node.namedChildren ?? [];
137
+ return children.some((child) => valueIsTainted(normalizeTraverseNode(child), rule));
138
+ };
139
+ const handleAssignment = (node, rule) => {
140
+ const { left, right } = getAssignmentSides(node, lang);
141
+ if (!left || !right)
142
+ return;
143
+ const targetNames = findIdentifiers(left, lang, source);
144
+ const tainted = valueIsTainted(right, rule);
145
+ for (const name of targetNames) {
146
+ if (tainted) {
147
+ taint(name);
148
+ taintedExpressions.add(right);
149
+ }
150
+ else {
151
+ untaint(name);
152
+ }
153
+ }
154
+ };
155
+ const reportFinding = (rule, node, message) => {
156
+ const loc = node.startPosition;
157
+ findings.push({
158
+ ruleId: rule.id,
159
+ ruleTitle: rule.title,
160
+ severity: rule.severity,
161
+ owasp: rule.owasp,
162
+ file: filePath,
163
+ line: loc ? loc.row + 1 : undefined,
164
+ column: loc ? loc.column + 1 : undefined,
165
+ message
166
+ });
167
+ };
168
+ const runRule = (rule) => {
169
+ if (rule.kind === "secret") {
170
+ const pattern = rule.literalPattern ? new RegExp(rule.literalPattern, "i") : null;
171
+ if (!pattern)
172
+ return;
173
+ walk(root, (node) => {
174
+ if (!isStringNode(node, lang))
175
+ return;
176
+ const text = getNodeText(node, source);
177
+ if (pattern.test(text)) {
178
+ reportFinding(rule, node, rule.title);
179
+ }
180
+ });
181
+ return;
182
+ }
183
+ if (rule.kind === "direct") {
184
+ const matcher = {
185
+ callee: rule.callee,
186
+ calleePrefix: rule.calleePrefix,
187
+ calleePattern: rule.calleePattern
188
+ };
189
+ walk(root, (node) => {
190
+ if (!lang.callNodes.includes(node.type))
191
+ return;
192
+ const name = getCallName(node, lang, source);
193
+ if (name && matchesCallee(name, matcher)) {
194
+ reportFinding(rule, node, rule.title);
195
+ }
196
+ });
197
+ return;
198
+ }
199
+ walk(root, (node) => {
200
+ if (lang.assignmentNodes.includes(node.type)) {
201
+ handleAssignment(node, rule);
202
+ return;
203
+ }
204
+ if (lang.callNodes.includes(node.type)) {
205
+ const calleeName = getCallName(node, lang, source);
206
+ if (!calleeName)
207
+ return;
208
+ const sink = rule.sinks?.find((candidate) => matchesCallee(calleeName, candidate.matcher));
209
+ if (!sink)
210
+ return;
211
+ const args = getCallArguments(node, lang);
212
+ const hasTainted = args.some((arg) => valueIsTainted(arg, rule));
213
+ if (hasTainted) {
214
+ reportFinding(rule, node, `Tainted data reaches sink ${sink.name}`);
215
+ }
216
+ }
217
+ });
218
+ };
219
+ for (const rule of ruleSet.rules) {
220
+ taintedVarsStack.length = 0;
221
+ taintedVarsStack.push(new Set());
222
+ runRule(rule);
223
+ }
224
+ return findings;
225
+ }
package/dist/scan.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
3
4
  import traverseImport from "@babel/traverse";
4
5
  import { execFile } from "node:child_process";
5
6
  import { promisify } from "node:util";
@@ -10,8 +11,15 @@ import { walkFiles } from "./fileWalker.js";
10
11
  import { parseSource } from "./analysis/ast.js";
11
12
  import { runRuleEngine } from "./analysis/rules.js";
12
13
  import { runPatternDetectors } from "./analysis/patterns.js";
14
+ import { runUniversalPatterns } from "./analysis/universalPatterns.js";
15
+ import { runInfraPatterns } from "./analysis/infraPatterns.js";
13
16
  import { loadRules } from "./rules/loadRules.js";
14
17
  import { scanDependenciesWithCves } from "./deps/engine.js";
18
+ import { runExternalAdapters } from "./adapters/runner.js";
19
+ import { getLanguageByExtension, getNativeLanguages } from "./native/languages.js";
20
+ import { parseWithTreeSitter } from "./native/loader.js";
21
+ import { loadNativeRules } from "./native/rules.js";
22
+ import { runNativeTaint } from "./native/taint.js";
15
23
  export const SCHEMA_VERSION = "1.0.0";
16
24
  const DEFAULT_MAX_CHARS = 4000;
17
25
  const DEFAULT_CONCURRENCY = 2;
@@ -20,6 +28,8 @@ const DEFAULT_RETRY_DELAY_MS = 500;
20
28
  const MAX_ESTIMATED_TOKENS = 50000; // Guardrail: reject massive scans
21
29
  const CHARS_PER_TOKEN = 4; // Simple heuristic for estimation
22
30
  export async function scan(options = {}) {
31
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
32
+ const outputFormat = options.format ?? "text";
23
33
  const resolvedRoot = options.targetPath
24
34
  ? path.resolve(options.cwd ?? process.cwd(), options.targetPath)
25
35
  : (options.cwd ?? process.cwd());
@@ -38,6 +48,13 @@ export async function scan(options = {}) {
38
48
  const concurrency = Math.max(1, options.concurrency ?? projectConfig.concurrency ?? DEFAULT_CONCURRENCY);
39
49
  const maxRetries = Math.max(0, options.maxRetries ?? DEFAULT_MAX_RETRIES);
40
50
  const retryDelayMs = Math.max(0, options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS);
51
+ const nativeTaintEnabled = options.nativeTaint ?? projectConfig.nativeTaint ?? true;
52
+ const nativeLangs = resolveNativeLanguages(options.nativeTaintLanguages ?? projectConfig.nativeTaintLanguages);
53
+ const nativeCacheEnabled = options.nativeTaintCache ?? projectConfig.nativeTaintCache ?? true;
54
+ const nativeCachePath = resolveNativeCachePath(cwd, options.nativeTaintCachePath ?? projectConfig.nativeTaintCachePath);
55
+ const nativeCache = nativeCacheEnabled
56
+ ? await loadNativeCache(nativeCachePath)
57
+ : { entries: new Map() };
41
58
  let files = await walkFiles(cwd, filters);
42
59
  if (options.diffOnly) {
43
60
  const changed = await getChangedFiles(cwd, options.diffBase ?? "HEAD");
@@ -128,6 +145,110 @@ export async function scan(options = {}) {
128
145
  category: "code"
129
146
  });
130
147
  }
148
+ const universalFindings = runUniversalPatterns(file.content, file.relPath);
149
+ for (const finding of universalFindings) {
150
+ findings.push({
151
+ id: finding.id,
152
+ severity: finding.severity,
153
+ title: finding.title,
154
+ description: `${finding.description} [${finding.owasp}]`,
155
+ file: finding.file,
156
+ line: finding.line,
157
+ owasp: finding.owasp,
158
+ category: "code"
159
+ });
160
+ }
161
+ if (isInfraFile(file.relPath)) {
162
+ const infraFindings = runInfraPatterns(file.content, file.relPath);
163
+ for (const finding of infraFindings) {
164
+ findings.push({
165
+ id: finding.id,
166
+ severity: finding.severity,
167
+ title: finding.title,
168
+ description: `${finding.description} [${finding.owasp}]`,
169
+ file: finding.file,
170
+ line: finding.line,
171
+ owasp: finding.owasp,
172
+ category: "code"
173
+ });
174
+ }
175
+ }
176
+ }
177
+ const adaptersEnabled = !(options.noAdapters ?? projectConfig.noAdapters ?? false);
178
+ if (adaptersEnabled) {
179
+ const adapterFiles = files.filter((filePath) => isLikelyTextFile(filePath));
180
+ const { findings: adapterFindings, warnings } = await runExternalAdapters({
181
+ cwd,
182
+ files: adapterFiles,
183
+ allowList: options.adapters ?? projectConfig.adapters
184
+ });
185
+ for (const warning of warnings) {
186
+ if (outputFormat !== "json" && outputFormat !== "sarif") {
187
+ console.warn(warning);
188
+ }
189
+ }
190
+ findings.push(...adapterFindings);
191
+ }
192
+ if (nativeTaintEnabled) {
193
+ const nativeWarnings = new Set();
194
+ const ruleCache = new Map();
195
+ const langConfigs = getNativeLanguages();
196
+ const langById = new Map(langConfigs.map((lang) => [lang.id, lang]));
197
+ const nativeFiles = files.filter((filePath) => {
198
+ const lang = getLanguageByExtension(filePath);
199
+ return Boolean(lang && nativeLangs.has(lang.id) && isLikelyTextFile(filePath));
200
+ });
201
+ for (const filePath of nativeFiles) {
202
+ const lang = getLanguageByExtension(filePath);
203
+ if (!lang || !nativeLangs.has(lang.id))
204
+ continue;
205
+ const relPath = path.relative(cwd, filePath);
206
+ const content = await fs.readFile(filePath, "utf8");
207
+ const cacheKey = computeNativeCacheKey(lang.id, content);
208
+ const cached = nativeCache.entries.get(relPath);
209
+ if (cached && cached.hash === cacheKey) {
210
+ const cachedFindings = cached.findings.map((f) => ({ ...f }));
211
+ for (const finding of cachedFindings)
212
+ findings.push(finding);
213
+ continue;
214
+ }
215
+ const parsed = await parseWithTreeSitter(content, lang, packageRoot);
216
+ if (!parsed) {
217
+ nativeWarnings.add(`Native taint skipped: missing parser for ${lang.name}. Run npm run build-grammars or install tree-sitter language bindings.`);
218
+ continue;
219
+ }
220
+ let rules = ruleCache.get(lang.id);
221
+ if (!rules) {
222
+ rules = await loadNativeRules(packageRoot, lang.id);
223
+ ruleCache.set(lang.id, rules);
224
+ }
225
+ if (!rules) {
226
+ nativeWarnings.add(`Native taint skipped: no rules for ${lang.name}.`);
227
+ continue;
228
+ }
229
+ const nativeFindings = runNativeTaint(parsed.tree, content, lang, rules, relPath);
230
+ const mapped = nativeFindings.map((finding) => ({
231
+ id: finding.ruleId,
232
+ severity: finding.severity,
233
+ title: finding.ruleTitle,
234
+ description: `${finding.message} [${finding.owasp}]`,
235
+ file: finding.file,
236
+ line: finding.line,
237
+ column: finding.column,
238
+ owasp: finding.owasp,
239
+ category: "code"
240
+ }));
241
+ for (const finding of mapped)
242
+ findings.push(finding);
243
+ if (nativeCacheEnabled) {
244
+ nativeCache.entries.set(relPath, { hash: cacheKey, findings: mapped });
245
+ }
246
+ }
247
+ if (nativeWarnings.size && outputFormat !== "json" && outputFormat !== "sarif") {
248
+ for (const warning of nativeWarnings) {
249
+ console.warn(warning);
250
+ }
251
+ }
131
252
  }
132
253
  if ((apiKey || useCodexCli) && !options.noAi) {
133
254
  if (options.aiMultiAgent) {
@@ -258,6 +379,34 @@ export async function scan(options = {}) {
258
379
  for (const filePath of nonJsFiles) {
259
380
  const relPath = path.relative(cwd, filePath);
260
381
  const content = await fs.readFile(filePath, "utf8");
382
+ const universalFindings = runUniversalPatterns(content, relPath);
383
+ for (const finding of universalFindings) {
384
+ findings.push({
385
+ id: finding.id,
386
+ severity: finding.severity,
387
+ title: finding.title,
388
+ description: `${finding.description} [${finding.owasp}]`,
389
+ file: finding.file,
390
+ line: finding.line,
391
+ owasp: finding.owasp,
392
+ category: "code"
393
+ });
394
+ }
395
+ if (isInfraFile(relPath)) {
396
+ const infraFindings = runInfraPatterns(content, relPath);
397
+ for (const finding of infraFindings) {
398
+ findings.push({
399
+ id: finding.id,
400
+ severity: finding.severity,
401
+ title: finding.title,
402
+ description: `${finding.description} [${finding.owasp}]`,
403
+ file: finding.file,
404
+ line: finding.line,
405
+ owasp: finding.owasp,
406
+ category: "code"
407
+ });
408
+ }
409
+ }
261
410
  const chunks = chunkText(content, maxChars);
262
411
  for (let i = 0; i < chunks.length; i += 1) {
263
412
  const prompt = buildPrompt(relPath, chunks[i], i + 1, chunks.length);
@@ -355,6 +504,9 @@ export async function scan(options = {}) {
355
504
  }
356
505
  await saveAiCache(cachePath, aiCache);
357
506
  }
507
+ if (nativeCacheEnabled) {
508
+ await saveNativeCache(nativeCachePath, nativeCache);
509
+ }
358
510
  return { findings: dedupeFindings(findings) };
359
511
  }
360
512
  export async function listMatchedFiles(options = {}) {
@@ -956,6 +1108,19 @@ function isLikelyTextFile(filePath) {
956
1108
  ]);
957
1109
  return !blocked.has(ext);
958
1110
  }
1111
+ function isInfraFile(filePath) {
1112
+ const normalized = filePath.split(path.sep).join("/");
1113
+ const base = path.basename(normalized).toLowerCase();
1114
+ if (base === "dockerfile" || base.endsWith(".dockerfile"))
1115
+ return true;
1116
+ if (base.endsWith(".tf") || base.endsWith(".tfvars"))
1117
+ return true;
1118
+ if (base.endsWith(".yaml") || base.endsWith(".yml"))
1119
+ return true;
1120
+ if (normalized.includes("/k8s/") || normalized.includes("/kubernetes/") || normalized.includes("/helm/"))
1121
+ return true;
1122
+ return false;
1123
+ }
959
1124
  export function groupFilesByModule(relPaths, depth) {
960
1125
  const groups = new Map();
961
1126
  for (const relPath of relPaths) {
@@ -998,6 +1163,12 @@ function resolveCachePath(cwd, override) {
998
1163
  }
999
1164
  return path.join(cwd, ".opensecurity", "ai-cache.json");
1000
1165
  }
1166
+ function resolveNativeCachePath(cwd, override) {
1167
+ if (override && override.trim()) {
1168
+ return path.isAbsolute(override) ? override : path.join(cwd, override);
1169
+ }
1170
+ return path.join(cwd, ".opensecurity", "native-taint-cache.json");
1171
+ }
1001
1172
  async function loadAiCache(cachePath) {
1002
1173
  try {
1003
1174
  const raw = await fs.readFile(cachePath, "utf8");
@@ -1019,6 +1190,27 @@ async function saveAiCache(cachePath, cache) {
1019
1190
  await fs.mkdir(path.dirname(cachePath), { recursive: true });
1020
1191
  await fs.writeFile(cachePath, JSON.stringify(obj, null, 2), "utf8");
1021
1192
  }
1193
+ async function loadNativeCache(cachePath) {
1194
+ try {
1195
+ const raw = await fs.readFile(cachePath, "utf8");
1196
+ const parsed = JSON.parse(raw);
1197
+ const entries = new Map(Object.entries(parsed ?? {}));
1198
+ return { entries };
1199
+ }
1200
+ catch (err) {
1201
+ if (err?.code === "ENOENT")
1202
+ return { entries: new Map() };
1203
+ return { entries: new Map() };
1204
+ }
1205
+ }
1206
+ async function saveNativeCache(cachePath, cache) {
1207
+ const obj = {};
1208
+ for (const [key, value] of cache.entries.entries()) {
1209
+ obj[key] = value;
1210
+ }
1211
+ await fs.mkdir(path.dirname(cachePath), { recursive: true });
1212
+ await fs.writeFile(cachePath, JSON.stringify(obj, null, 2), "utf8");
1213
+ }
1022
1214
  function computeCacheKey(provider, model, content) {
1023
1215
  return crypto
1024
1216
  .createHash("sha256")
@@ -1029,6 +1221,21 @@ function computeCacheKey(provider, model, content) {
1029
1221
  .update(content)
1030
1222
  .digest("hex");
1031
1223
  }
1224
+ function computeNativeCacheKey(language, content) {
1225
+ return crypto
1226
+ .createHash("sha256")
1227
+ .update(language)
1228
+ .update("|")
1229
+ .update(content)
1230
+ .digest("hex");
1231
+ }
1232
+ function resolveNativeLanguages(list) {
1233
+ const all = getNativeLanguages().map((lang) => lang.id);
1234
+ if (!list || list.length === 0)
1235
+ return new Set(all);
1236
+ const normalized = new Set(list.map((item) => item.trim().toLowerCase()).filter(Boolean));
1237
+ return new Set(all.filter((lang) => normalized.has(lang)));
1238
+ }
1032
1239
  async function safeStat(target) {
1033
1240
  try {
1034
1241
  return await fs.stat(target);
package/package.json CHANGED
@@ -1,8 +1,33 @@
1
1
  {
2
2
  "name": "opensecurity",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
+ "description": "Open-source CLI for scanning repositories for security risks across code, infra, and dependencies.",
6
+ "license": "MIT",
5
7
  "type": "module",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/eliophan/opensecurity.git"
11
+ },
12
+ "homepage": "https://github.com/eliophan/opensecurity",
13
+ "bugs": {
14
+ "url": "https://github.com/eliophan/opensecurity/issues"
15
+ },
16
+ "keywords": [
17
+ "security",
18
+ "sast",
19
+ "static-analysis",
20
+ "taint",
21
+ "vulnerability",
22
+ "scanner",
23
+ "devsecops",
24
+ "cli",
25
+ "dependency",
26
+ "ai"
27
+ ],
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
6
31
  "bin": {
7
32
  "opensecurity": "dist/cli.js",
8
33
  "openSecurity": "dist/cli.js"
@@ -11,6 +36,7 @@
11
36
  "dev": "tsx src/cli.ts",
12
37
  "proxy": "tsx src/proxy.ts",
13
38
  "build": "tsc",
39
+ "build-grammars": "tsx scripts/build-grammars.ts",
14
40
  "prepack": "npm run build",
15
41
  "package": "npm run build && npm pack",
16
42
  "test": "vitest run",
@@ -18,6 +44,8 @@
18
44
  },
19
45
  "files": [
20
46
  "dist",
47
+ "assets",
48
+ "rules",
21
49
  "README.md",
22
50
  "LICENSE"
23
51
  ],
@@ -27,7 +55,11 @@
27
55
  "@babel/types": "^7.26.9",
28
56
  "commander": "^12.0.0",
29
57
  "picomatch": "^4.0.2",
30
- "semver": "^7.6.3"
58
+ "semver": "^7.6.3",
59
+ "web-tree-sitter": "^0.21.0"
60
+ },
61
+ "optionalDependencies": {
62
+ "tree-sitter": "^0.21.1"
31
63
  },
32
64
  "devDependencies": {
33
65
  "@types/babel__traverse": "^7.20.7",
@@ -37,6 +69,18 @@
37
69
  "@typescript-eslint/eslint-plugin": "^8.57.0",
38
70
  "@typescript-eslint/parser": "^8.57.0",
39
71
  "eslint": "^9.39.4",
72
+ "tree-sitter-cli": "^0.21.0",
73
+ "tree-sitter-c": "^0.21.4",
74
+ "tree-sitter-c-sharp": "^0.23.1",
75
+ "tree-sitter-cpp": "^0.22.3",
76
+ "tree-sitter-go": "^0.21.2",
77
+ "tree-sitter-java": "^0.21.0",
78
+ "tree-sitter-kotlin": "^0.3.8",
79
+ "tree-sitter-php": "^0.22.8",
80
+ "tree-sitter-python": "^0.21.0",
81
+ "tree-sitter-ruby": "^0.21.0",
82
+ "tree-sitter-rust": "^0.24.0",
83
+ "tree-sitter-swift": "^0.7.1",
40
84
  "tsx": "^4.19.0",
41
85
  "typescript": "^5.7.3",
42
86
  "vitest": "^2.1.8"
@@ -0,0 +1,47 @@
1
+ {
2
+ "language": "c",
3
+ "rules": [
4
+ {
5
+ "id": "c-cmd",
6
+ "title": "Command Injection",
7
+ "severity": "high",
8
+ "owasp": "A03:2021 Injection",
9
+ "kind": "taint",
10
+ "sources": [
11
+ { "id": "argv", "name": "argv", "matcher": { "calleePattern": ["argv", "getenv"] } }
12
+ ],
13
+ "sinks": [
14
+ { "id": "system", "name": "system", "matcher": { "callee": ["system", "popen"] } }
15
+ ]
16
+ },
17
+ {
18
+ "id": "c-path",
19
+ "title": "Path Traversal",
20
+ "severity": "high",
21
+ "owasp": "A01:2021 Broken Access Control",
22
+ "kind": "taint",
23
+ "sources": [
24
+ { "id": "argv", "name": "argv", "matcher": { "calleePattern": ["argv", "getenv"] } }
25
+ ],
26
+ "sinks": [
27
+ { "id": "fopen", "name": "fopen", "matcher": { "callee": ["fopen", "open", "read"] } }
28
+ ]
29
+ },
30
+ {
31
+ "id": "c-weak-crypto",
32
+ "title": "Weak Crypto",
33
+ "severity": "medium",
34
+ "owasp": "A02:2021 Cryptographic Failures",
35
+ "kind": "direct",
36
+ "calleePattern": ["*MD5*", "*SHA1*", "MD5", "SHA1"]
37
+ },
38
+ {
39
+ "id": "c-hardcoded-secret",
40
+ "title": "Hardcoded Secret",
41
+ "severity": "medium",
42
+ "owasp": "A02:2021 Cryptographic Failures",
43
+ "kind": "secret",
44
+ "literalPattern": "(api|secret|token|password|passwd|pwd|key)[^\\n\\r]{0,20}[\"'][A-Za-z0-9_\\-]{8,}[\"']"
45
+ }
46
+ ]
47
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "language": "cpp",
3
+ "rules": [
4
+ {
5
+ "id": "cpp-cmd",
6
+ "title": "Command Injection",
7
+ "severity": "high",
8
+ "owasp": "A03:2021 Injection",
9
+ "kind": "taint",
10
+ "sources": [
11
+ { "id": "argv", "name": "argv", "matcher": { "calleePattern": ["argv", "getenv"] } }
12
+ ],
13
+ "sinks": [
14
+ { "id": "system", "name": "system", "matcher": { "callee": ["system", "popen"] } }
15
+ ]
16
+ },
17
+ {
18
+ "id": "cpp-path",
19
+ "title": "Path Traversal",
20
+ "severity": "high",
21
+ "owasp": "A01:2021 Broken Access Control",
22
+ "kind": "taint",
23
+ "sources": [
24
+ { "id": "argv", "name": "argv", "matcher": { "calleePattern": ["argv", "getenv"] } }
25
+ ],
26
+ "sinks": [
27
+ { "id": "fstream", "name": "fstream", "matcher": { "calleePattern": ["std::fstream", "std::ifstream", "std::ofstream", "fopen", "open"] } }
28
+ ]
29
+ },
30
+ {
31
+ "id": "cpp-weak-crypto",
32
+ "title": "Weak Crypto",
33
+ "severity": "medium",
34
+ "owasp": "A02:2021 Cryptographic Failures",
35
+ "kind": "direct",
36
+ "calleePattern": ["*MD5*", "*SHA1*", "MD5", "SHA1"]
37
+ },
38
+ {
39
+ "id": "cpp-hardcoded-secret",
40
+ "title": "Hardcoded Secret",
41
+ "severity": "medium",
42
+ "owasp": "A02:2021 Cryptographic Failures",
43
+ "kind": "secret",
44
+ "literalPattern": "(api|secret|token|password|passwd|pwd|key)[^\\n\\r]{0,20}[\"'][A-Za-z0-9_\\-]{8,}[\"']"
45
+ }
46
+ ]
47
+ }