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.
@@ -0,0 +1,230 @@
1
+ import traverseImport from "@babel/traverse";
2
+ import * as t from "@babel/types";
3
+ const SECRET_NAME_REGEX = /^(api[-_]?key|secret|token|password|passwd|pwd|private[-_]?key|access[-_]?key|client[-_]?secret)$/i;
4
+ const SECRET_VALUE_MIN_LEN = 16;
5
+ const SECRET_ENTROPY_THRESHOLD = 3.7;
6
+ const SECRET_VALUE_PATTERNS = [
7
+ { id: "secret-aws-access-key", pattern: /AKIA[0-9A-Z]{16}/, title: "Hardcoded AWS Access Key" },
8
+ { id: "secret-github", pattern: /gh[pousr]_[A-Za-z0-9]{20,}/, title: "Hardcoded GitHub Token" },
9
+ { id: "secret-slack", pattern: /xox[baprs]-[A-Za-z0-9-]{10,}/, title: "Hardcoded Slack Token" },
10
+ { id: "secret-stripe", pattern: /sk_live_[A-Za-z0-9]{16,}/, title: "Hardcoded Stripe Secret Key" },
11
+ { id: "secret-google-api", pattern: /AIza[0-9A-Za-z\-_]{35}/, title: "Hardcoded Google API Key" },
12
+ { id: "secret-jwt", pattern: /eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/, title: "Hardcoded JWT" },
13
+ { id: "secret-private-key", pattern: /-----BEGIN (RSA|EC|DSA|PRIVATE) KEY-----/, title: "Hardcoded Private Key" }
14
+ ];
15
+ const WEAK_HASHES = new Set(["md5", "sha1", "md4"]);
16
+ const WEAK_CIPHERS = ["des", "3des", "rc2", "rc4", "bf", "blowfish", "idea"];
17
+ const INSECURE_RANDOM_CALLS = new Set(["Math.random", "crypto.pseudoRandomBytes", "pseudoRandomBytes"]);
18
+ const DESERIALIZATION_CALLEES = new Set([
19
+ "unserialize",
20
+ "deserialize",
21
+ "serialize.unserialize",
22
+ "serialize.deserialize",
23
+ "yaml.load",
24
+ "YAML.load",
25
+ "jsyaml.load"
26
+ ]);
27
+ export function runPatternDetectors(ast, filePath) {
28
+ const traverse = normalizeTraverse(traverseImport);
29
+ const findings = [];
30
+ const pushFinding = (finding) => {
31
+ findings.push(finding);
32
+ };
33
+ const markSecret = (node, title, description, id = "hardcoded-secret") => {
34
+ const loc = node.loc?.start;
35
+ pushFinding({
36
+ id,
37
+ severity: "high",
38
+ owasp: "A07:2021 Identification and Authentication Failures",
39
+ title,
40
+ description,
41
+ file: filePath,
42
+ line: loc?.line,
43
+ column: typeof loc?.column === "number" ? loc.column + 1 : undefined
44
+ });
45
+ };
46
+ const markCrypto = (node, title, description, id = "insecure-crypto") => {
47
+ const loc = node.loc?.start;
48
+ pushFinding({
49
+ id,
50
+ severity: "high",
51
+ owasp: "A02:2021 Cryptographic Failures",
52
+ title,
53
+ description,
54
+ file: filePath,
55
+ line: loc?.line,
56
+ column: typeof loc?.column === "number" ? loc.column + 1 : undefined
57
+ });
58
+ };
59
+ const markDeserialize = (node, title, description, id = "unsafe-deserialization") => {
60
+ const loc = node.loc?.start;
61
+ pushFinding({
62
+ id,
63
+ severity: "high",
64
+ owasp: "A08:2021 Software and Data Integrity Failures",
65
+ title,
66
+ description,
67
+ file: filePath,
68
+ line: loc?.line,
69
+ column: typeof loc?.column === "number" ? loc.column + 1 : undefined
70
+ });
71
+ };
72
+ const getStringValue = (node) => {
73
+ if (t.isStringLiteral(node))
74
+ return node.value;
75
+ if (t.isTemplateLiteral(node) && node.expressions.length === 0) {
76
+ return node.quasis.map((q) => q.value.cooked ?? "").join("");
77
+ }
78
+ return null;
79
+ };
80
+ const isSecretKeyName = (name) => {
81
+ if (!name)
82
+ return false;
83
+ return SECRET_NAME_REGEX.test(name);
84
+ };
85
+ const isHighEntropySecret = (value) => {
86
+ if (value.length < SECRET_VALUE_MIN_LEN)
87
+ return false;
88
+ return shannonEntropy(value) >= SECRET_ENTROPY_THRESHOLD;
89
+ };
90
+ const matchesSecretValue = (value) => {
91
+ for (const entry of SECRET_VALUE_PATTERNS) {
92
+ if (entry.pattern.test(value))
93
+ return { id: entry.id, title: entry.title };
94
+ }
95
+ return null;
96
+ };
97
+ traverse(ast, {
98
+ VariableDeclarator(path) {
99
+ if (!t.isIdentifier(path.node.id))
100
+ return;
101
+ if (!path.node.init)
102
+ return;
103
+ const value = getStringValue(path.node.init);
104
+ if (!value)
105
+ return;
106
+ if (value.length < SECRET_VALUE_MIN_LEN)
107
+ return;
108
+ const name = path.node.id.name;
109
+ const matched = matchesSecretValue(value);
110
+ if (matched) {
111
+ markSecret(path.node.init, matched.title, `Detected ${matched.title.toLowerCase()} in code.`, matched.id);
112
+ return;
113
+ }
114
+ if (isSecretKeyName(name)) {
115
+ markSecret(path.node.init, "Hardcoded Secret", `Hardcoded value assigned to '${name}'.`);
116
+ return;
117
+ }
118
+ if (isHighEntropySecret(value)) {
119
+ markSecret(path.node.init, "Hardcoded Secret", "High-entropy string literal may be a secret.");
120
+ }
121
+ },
122
+ ObjectProperty(path) {
123
+ if (!t.isExpression(path.node.value))
124
+ return;
125
+ const value = getStringValue(path.node.value);
126
+ if (!value || value.length < SECRET_VALUE_MIN_LEN)
127
+ return;
128
+ let keyName = null;
129
+ if (t.isIdentifier(path.node.key))
130
+ keyName = path.node.key.name;
131
+ if (t.isStringLiteral(path.node.key))
132
+ keyName = path.node.key.value;
133
+ const matched = matchesSecretValue(value);
134
+ if (matched) {
135
+ markSecret(path.node.value, matched.title, `Detected ${matched.title.toLowerCase()} in object literal.`, matched.id);
136
+ return;
137
+ }
138
+ if (isSecretKeyName(keyName)) {
139
+ markSecret(path.node.value, "Hardcoded Secret", `Hardcoded value for object key '${keyName}'.`);
140
+ return;
141
+ }
142
+ if (isHighEntropySecret(value)) {
143
+ markSecret(path.node.value, "Hardcoded Secret", "High-entropy string literal may be a secret.");
144
+ }
145
+ },
146
+ CallExpression(path) {
147
+ const calleeName = getCalleeName(path.node);
148
+ if (!calleeName)
149
+ return;
150
+ if (calleeName === "crypto.createHash" || calleeName === "createHash") {
151
+ const arg = path.node.arguments[0];
152
+ if (arg && t.isExpression(arg)) {
153
+ const value = getStringValue(arg);
154
+ if (value && WEAK_HASHES.has(value.toLowerCase())) {
155
+ markCrypto(arg, "Weak Hash Function", `Hash algorithm '${value}' is considered weak.`);
156
+ }
157
+ }
158
+ }
159
+ if (INSECURE_RANDOM_CALLS.has(calleeName)) {
160
+ markCrypto(path.node, "Insecure Randomness", "Math.random or pseudoRandomBytes is not cryptographically secure.");
161
+ }
162
+ if (calleeName === "crypto.createCipher" || calleeName === "createCipher") {
163
+ markCrypto(path.node, "Insecure Cipher API", "crypto.createCipher is deprecated and insecure.");
164
+ }
165
+ if (calleeName === "crypto.createDecipher" || calleeName === "createDecipher") {
166
+ markCrypto(path.node, "Insecure Cipher API", "crypto.createDecipher is deprecated and insecure.");
167
+ }
168
+ if (calleeName === "crypto.createCipheriv" ||
169
+ calleeName === "crypto.createDecipheriv" ||
170
+ calleeName === "createCipheriv" ||
171
+ calleeName === "createDecipheriv") {
172
+ const arg = path.node.arguments[0];
173
+ if (arg && t.isExpression(arg)) {
174
+ const value = getStringValue(arg);
175
+ if (value) {
176
+ const lower = value.toLowerCase();
177
+ const weak = WEAK_CIPHERS.some((alg) => lower.includes(alg)) || lower.includes("-ecb") || lower.endsWith("ecb");
178
+ if (weak) {
179
+ markCrypto(arg, "Weak Cipher Algorithm", `Cipher algorithm '${value}' is considered weak.`);
180
+ }
181
+ }
182
+ }
183
+ }
184
+ if (DESERIALIZATION_CALLEES.has(calleeName)) {
185
+ markDeserialize(path.node, "Unsafe Deserialization", `Call to '${calleeName}' may deserialize untrusted data.`);
186
+ }
187
+ }
188
+ });
189
+ return findings;
190
+ }
191
+ function getCalleeName(node) {
192
+ const callee = node.callee;
193
+ if (t.isIdentifier(callee))
194
+ return callee.name;
195
+ if (t.isMemberExpression(callee))
196
+ return memberExpressionToString(callee);
197
+ return null;
198
+ }
199
+ function memberExpressionToString(node) {
200
+ if (node.computed)
201
+ return null;
202
+ const object = node.object;
203
+ const property = node.property;
204
+ const objectName = t.isIdentifier(object)
205
+ ? object.name
206
+ : t.isMemberExpression(object)
207
+ ? memberExpressionToString(object)
208
+ : null;
209
+ if (!objectName)
210
+ return null;
211
+ if (!t.isIdentifier(property))
212
+ return null;
213
+ return `${objectName}.${property.name}`;
214
+ }
215
+ function normalizeTraverse(value) {
216
+ return value.default ?? value;
217
+ }
218
+ function shannonEntropy(value) {
219
+ const counts = new Map();
220
+ for (const ch of value) {
221
+ counts.set(ch, (counts.get(ch) ?? 0) + 1);
222
+ }
223
+ const len = value.length;
224
+ let entropy = 0;
225
+ for (const count of counts.values()) {
226
+ const p = count / len;
227
+ entropy -= p * Math.log2(p);
228
+ }
229
+ return entropy;
230
+ }
@@ -0,0 +1,48 @@
1
+ import { runTaintAnalysis } from "./taint.js";
2
+ export const OWASP_TOP_10 = [
3
+ "A01:2021 Broken Access Control",
4
+ "A02:2021 Cryptographic Failures",
5
+ "A03:2021 Injection",
6
+ "A04:2021 Insecure Design",
7
+ "A05:2021 Security Misconfiguration",
8
+ "A06:2021 Vulnerable and Outdated Components",
9
+ "A07:2021 Identification and Authentication Failures",
10
+ "A08:2021 Software and Data Integrity Failures",
11
+ "A09:2021 Security Logging and Monitoring Failures",
12
+ "A10:2021 Server-Side Request Forgery"
13
+ ];
14
+ export function runRuleEngine(ast, filePath, rules) {
15
+ const findings = [];
16
+ for (const rule of rules) {
17
+ const taintFindings = runTaintAnalysis(ast, filePath, {
18
+ sources: rule.sources,
19
+ sinks: rule.sinks,
20
+ sanitizers: rule.sanitizers
21
+ });
22
+ for (const finding of taintFindings) {
23
+ findings.push({
24
+ ruleId: rule.id,
25
+ ruleName: rule.name,
26
+ owasp: rule.owasp,
27
+ severity: rule.severity,
28
+ file: filePath,
29
+ line: finding.line,
30
+ column: finding.column,
31
+ message: `${rule.name}: ${finding.message}`
32
+ });
33
+ }
34
+ }
35
+ return findings;
36
+ }
37
+ export function mapFindingsToOwasp(findings) {
38
+ const map = new Map();
39
+ for (const category of OWASP_TOP_10) {
40
+ map.set(category, []);
41
+ }
42
+ for (const finding of findings) {
43
+ const bucket = map.get(finding.owasp);
44
+ if (bucket)
45
+ bucket.push(finding);
46
+ }
47
+ return map;
48
+ }
@@ -0,0 +1,199 @@
1
+ import traverseImport from "@babel/traverse";
2
+ import * as t from "@babel/types";
3
+ export function runTaintAnalysis(ast, filePath, rules) {
4
+ const traverse = normalizeTraverse(traverseImport);
5
+ const findings = [];
6
+ const taintedVarsStack = [];
7
+ const taintedExpressions = new WeakSet();
8
+ const pushScope = () => taintedVarsStack.push(new Set());
9
+ const popScope = () => taintedVarsStack.pop();
10
+ const currentScope = () => taintedVarsStack[taintedVarsStack.length - 1];
11
+ const isTainted = (name) => currentScope()?.has(name) ?? false;
12
+ const taint = (name) => currentScope()?.add(name);
13
+ const untaint = (name) => currentScope()?.delete(name);
14
+ const matchEndpoint = (node, endpoints) => {
15
+ const calleeName = getCalleeName(node);
16
+ if (!calleeName)
17
+ return null;
18
+ return endpoints.find((endpoint) => matchesCallee(calleeName, endpoint.matcher));
19
+ };
20
+ const isSanitizerCall = (node) => matchEndpoint(node, rules.sanitizers);
21
+ const isSourceCall = (node) => matchEndpoint(node, rules.sources);
22
+ const isSinkCall = (node) => matchEndpoint(node, rules.sinks);
23
+ const valueIsTainted = (node) => {
24
+ if (taintedExpressions.has(node))
25
+ return true;
26
+ if (t.isIdentifier(node))
27
+ return isTainted(node.name);
28
+ if (t.isMemberExpression(node)) {
29
+ return t.isExpression(node.object) ? valueIsTainted(node.object) : false;
30
+ }
31
+ if (t.isCallExpression(node)) {
32
+ if (isSanitizerCall(node))
33
+ return false;
34
+ if (isSourceCall(node))
35
+ return true;
36
+ return false;
37
+ }
38
+ if (t.isBinaryExpression(node) || t.isLogicalExpression(node)) {
39
+ return valueIsTainted(node.left) || valueIsTainted(node.right);
40
+ }
41
+ if (t.isConditionalExpression(node)) {
42
+ return valueIsTainted(node.test) || valueIsTainted(node.consequent) || valueIsTainted(node.alternate);
43
+ }
44
+ if (t.isTemplateLiteral(node)) {
45
+ return node.expressions.some((expr) => valueIsTainted(expr));
46
+ }
47
+ if (t.isArrayExpression(node)) {
48
+ return node.elements.some((el) => (t.isExpression(el) ? valueIsTainted(el) : false));
49
+ }
50
+ if (t.isObjectExpression(node)) {
51
+ return node.properties.some((prop) => {
52
+ if (t.isObjectProperty(prop) && t.isExpression(prop.value))
53
+ return valueIsTainted(prop.value);
54
+ if (t.isSpreadElement(prop) && t.isExpression(prop.argument))
55
+ return valueIsTainted(prop.argument);
56
+ return false;
57
+ });
58
+ }
59
+ if (t.isUnaryExpression(node)) {
60
+ return t.isExpression(node.argument) ? valueIsTainted(node.argument) : false;
61
+ }
62
+ if (t.isSequenceExpression(node)) {
63
+ return node.expressions.some((expr) => valueIsTainted(expr));
64
+ }
65
+ return false;
66
+ };
67
+ const handleAssignment = (id, value) => {
68
+ if (t.isCallExpression(value)) {
69
+ if (isSanitizerCall(value)) {
70
+ untaint(id.name);
71
+ return;
72
+ }
73
+ if (isSourceCall(value)) {
74
+ taint(id.name);
75
+ taintedExpressions.add(value);
76
+ return;
77
+ }
78
+ }
79
+ if (valueIsTainted(value)) {
80
+ taint(id.name);
81
+ }
82
+ else {
83
+ untaint(id.name);
84
+ }
85
+ };
86
+ traverse(ast, {
87
+ Program: {
88
+ enter() {
89
+ pushScope();
90
+ },
91
+ exit() {
92
+ popScope();
93
+ }
94
+ },
95
+ Function: {
96
+ enter() {
97
+ pushScope();
98
+ },
99
+ exit() {
100
+ popScope();
101
+ }
102
+ },
103
+ VariableDeclarator(path) {
104
+ if (!path.node.init)
105
+ return;
106
+ if (!t.isIdentifier(path.node.id))
107
+ return;
108
+ handleAssignment(path.node.id, path.node.init);
109
+ },
110
+ AssignmentExpression(path) {
111
+ const left = path.node.left;
112
+ if (!t.isIdentifier(left))
113
+ return;
114
+ handleAssignment(left, path.node.right);
115
+ },
116
+ CallExpression(path) {
117
+ const sink = isSinkCall(path.node);
118
+ if (!sink)
119
+ return;
120
+ const args = path.node.arguments;
121
+ const hasTaintedArg = args.some((arg) => Boolean(t.isExpression(arg) && valueIsTainted(arg)));
122
+ if (!hasTaintedArg)
123
+ return;
124
+ const loc = path.node.loc?.start;
125
+ findings.push({
126
+ sourceId: "tainted",
127
+ sinkId: sink.id,
128
+ file: filePath,
129
+ line: loc?.line,
130
+ column: typeof loc?.column === "number" ? loc.column + 1 : undefined,
131
+ message: `Tainted data reaches sink ${sink.name}`
132
+ });
133
+ }
134
+ });
135
+ return findings;
136
+ }
137
+ function getCalleeName(node) {
138
+ const callee = node.callee;
139
+ if (t.isIdentifier(callee))
140
+ return callee.name;
141
+ if (t.isMemberExpression(callee))
142
+ return memberExpressionToString(callee);
143
+ return null;
144
+ }
145
+ function memberExpressionToString(node) {
146
+ if (node.computed)
147
+ return null;
148
+ const object = node.object;
149
+ const property = node.property;
150
+ const objectName = t.isIdentifier(object)
151
+ ? object.name
152
+ : t.isMemberExpression(object)
153
+ ? memberExpressionToString(object)
154
+ : null;
155
+ if (!objectName)
156
+ return null;
157
+ if (!t.isIdentifier(property))
158
+ return null;
159
+ return `${objectName}.${property.name}`;
160
+ }
161
+ function matchesCallee(calleeName, matcher) {
162
+ if (matcher.callee) {
163
+ const match = matcher.callee;
164
+ if (Array.isArray(match) && match.includes(calleeName))
165
+ return true;
166
+ if (match === calleeName)
167
+ return true;
168
+ }
169
+ if (matcher.calleePrefix) {
170
+ const match = matcher.calleePrefix;
171
+ if (Array.isArray(match) && match.some((prefix) => calleeName.startsWith(prefix)))
172
+ return true;
173
+ if (typeof match === "string" && calleeName.startsWith(match))
174
+ return true;
175
+ }
176
+ if (matcher.calleePattern) {
177
+ const match = matcher.calleePattern;
178
+ if (Array.isArray(match) && match.some((pattern) => matchGlob(calleeName, pattern)))
179
+ return true;
180
+ if (typeof match === "string" && matchGlob(calleeName, match))
181
+ return true;
182
+ }
183
+ return false;
184
+ }
185
+ function matchGlob(value, pattern) {
186
+ if (pattern === value)
187
+ return true;
188
+ try {
189
+ const picomatch = require("picomatch");
190
+ const isMatch = picomatch(pattern, { nocase: false, dot: true });
191
+ return isMatch(value);
192
+ }
193
+ catch {
194
+ return false;
195
+ }
196
+ }
197
+ function normalizeTraverse(value) {
198
+ return value.default ?? value;
199
+ }