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,114 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ export async function scanDependencies(root) {
4
+ const deps = [];
5
+ const pkgJsonPath = path.join(root, "package.json");
6
+ const pkgLockPath = path.join(root, "package-lock.json");
7
+ const requirementsPath = path.join(root, "requirements.txt");
8
+ const [pkgJson, pkgLock, requirements] = await Promise.all([
9
+ readJsonFile(pkgJsonPath),
10
+ readJsonFile(pkgLockPath),
11
+ readTextFile(requirementsPath)
12
+ ]);
13
+ const resolvedVersions = pkgLock ? parsePackageLock(pkgLock) : new Map();
14
+ if (pkgJson) {
15
+ const depsFromPkg = parsePackageJson(pkgJson, pkgJsonPath);
16
+ for (const dep of depsFromPkg) {
17
+ const resolved = resolvedVersions.get(dep.name);
18
+ deps.push({ ...dep, version: dep.version ?? resolved });
19
+ }
20
+ }
21
+ if (requirements) {
22
+ deps.push(...parseRequirements(requirements, requirementsPath));
23
+ }
24
+ return deps;
25
+ }
26
+ function parsePackageJson(data, source) {
27
+ const deps = [];
28
+ const addDeps = (record, scope) => {
29
+ if (!record)
30
+ return;
31
+ for (const [name, spec] of Object.entries(record)) {
32
+ deps.push({
33
+ name,
34
+ spec,
35
+ version: undefined,
36
+ ecosystem: "npm",
37
+ scope,
38
+ source
39
+ });
40
+ }
41
+ };
42
+ addDeps(data.dependencies, "prod");
43
+ addDeps(data.devDependencies, "dev");
44
+ return deps;
45
+ }
46
+ function parsePackageLock(data) {
47
+ const versions = new Map();
48
+ if (data.packages && typeof data.packages === "object") {
49
+ for (const [pkgPath, info] of Object.entries(data.packages)) {
50
+ if (!info || typeof info !== "object")
51
+ continue;
52
+ if (!info.name || !info.version)
53
+ continue;
54
+ versions.set(info.name, info.version);
55
+ if (pkgPath === "" && info.dependencies) {
56
+ for (const [name, version] of Object.entries(info.dependencies)) {
57
+ versions.set(name, version);
58
+ }
59
+ }
60
+ }
61
+ }
62
+ if (data.dependencies && typeof data.dependencies === "object") {
63
+ for (const [name, info] of Object.entries(data.dependencies)) {
64
+ if (info?.version) {
65
+ versions.set(name, info.version);
66
+ }
67
+ }
68
+ }
69
+ return versions;
70
+ }
71
+ function parseRequirements(text, source) {
72
+ const deps = [];
73
+ const lines = text.split(/\r?\n/);
74
+ for (const line of lines) {
75
+ const trimmed = line.trim();
76
+ if (!trimmed || trimmed.startsWith("#"))
77
+ continue;
78
+ const [namePart, versionPart] = trimmed.split(/==|>=|<=|~=|!=|>/).map((s) => s.trim());
79
+ if (!namePart)
80
+ continue;
81
+ const versionMatch = trimmed.match(/(==|>=|<=|~=|!=|>)(.+)$/);
82
+ const spec = versionMatch ? `${versionMatch[1]}${versionMatch[2].trim()}` : undefined;
83
+ deps.push({
84
+ name: namePart,
85
+ spec,
86
+ version: versionPart && versionMatch?.[1] === "==" ? versionPart : undefined,
87
+ ecosystem: "pypi",
88
+ scope: "prod",
89
+ source
90
+ });
91
+ }
92
+ return deps;
93
+ }
94
+ async function readJsonFile(filePath) {
95
+ try {
96
+ const raw = await fs.readFile(filePath, "utf8");
97
+ return JSON.parse(raw);
98
+ }
99
+ catch (err) {
100
+ if (err?.code === "ENOENT")
101
+ return null;
102
+ throw err;
103
+ }
104
+ }
105
+ async function readTextFile(filePath) {
106
+ try {
107
+ return await fs.readFile(filePath, "utf8");
108
+ }
109
+ catch (err) {
110
+ if (err?.code === "ENOENT")
111
+ return null;
112
+ throw err;
113
+ }
114
+ }
@@ -0,0 +1,46 @@
1
+ export function scoreRisk(cve, options = {}) {
2
+ const base = baseScore(cve);
3
+ const exploitability = cve.exploitability === "high" ? 10 : cve.exploitability === "medium" ? 5 : 0;
4
+ const privilegeRequired = cve.privilegeRequired === "none" ? 10 : cve.privilegeRequired === "low" ? 5 : 0;
5
+ const dataSensitivity = options.dataSensitivity === "high" ? 10 : options.dataSensitivity === "medium" ? 5 : 0;
6
+ const score = clamp(base + exploitability + privilegeRequired + dataSensitivity, 0, 100);
7
+ return {
8
+ score,
9
+ severity: scoreToSeverity(score),
10
+ factors: {
11
+ base,
12
+ exploitability,
13
+ privilegeRequired,
14
+ dataSensitivity
15
+ }
16
+ };
17
+ }
18
+ function baseScore(cve) {
19
+ if (typeof cve.cvssScore === "number" && !Number.isNaN(cve.cvssScore)) {
20
+ return clamp(cve.cvssScore * 10, 0, 100);
21
+ }
22
+ switch (cve.severity) {
23
+ case "critical":
24
+ return 90;
25
+ case "high":
26
+ return 70;
27
+ case "medium":
28
+ return 40;
29
+ case "low":
30
+ return 20;
31
+ default:
32
+ return 30;
33
+ }
34
+ }
35
+ function scoreToSeverity(score) {
36
+ if (score >= 90)
37
+ return "critical";
38
+ if (score >= 70)
39
+ return "high";
40
+ if (score >= 40)
41
+ return "medium";
42
+ return "low";
43
+ }
44
+ function clamp(value, min, max) {
45
+ return Math.min(max, Math.max(min, value));
46
+ }
@@ -0,0 +1,9 @@
1
+ export function buildSimulation(finding) {
2
+ const dep = finding.dependency;
3
+ const cve = finding.cve;
4
+ const payload = `Trigger ${cve.id} via ${dep.name}@${dep.version ?? dep.spec ?? "unknown"}`;
5
+ const impact = cve.description
6
+ ? `Potential impact: ${cve.description}`
7
+ : `Potential impact: untrusted input could exploit ${dep.name}.`;
8
+ return { payload, impact };
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import picomatch from "picomatch";
4
+ export async function walkFiles(rootDir, options) {
5
+ const includeMatchers = options.include.map((p) => picomatch(p, { dot: true }));
6
+ const excludeMatchers = options.exclude.map((p) => picomatch(p, { dot: true }));
7
+ const results = [];
8
+ async function visit(currentDir) {
9
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
10
+ for (const entry of entries) {
11
+ const fullPath = path.join(currentDir, entry.name);
12
+ const relPath = path.relative(rootDir, fullPath).split(path.sep).join("/");
13
+ if (excludeMatchers.some((m) => m(relPath))) {
14
+ continue;
15
+ }
16
+ if (entry.isDirectory()) {
17
+ await visit(fullPath);
18
+ continue;
19
+ }
20
+ if (includeMatchers.length === 0 || includeMatchers.some((m) => m(relPath))) {
21
+ results.push(fullPath);
22
+ }
23
+ }
24
+ }
25
+ await visit(rootDir);
26
+ return results;
27
+ }