Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

license-scan.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

license-scan.tsBlame227 lines · 1 contributor
7a1c00eClaude1/**
2 * License compliance scanner.
3 *
4 * Analyzes repo dependencies for license compatibility issues. Flags
5 * copyleft licenses (GPL, AGPL) that may contaminate proprietary codebases,
6 * and unknown/missing licenses that need manual review.
7 */
8
9import { listDependenciesForRepo } from "./deps";
10import type { RepoDependency } from "../db/schema";
11
12export type LicenseRisk = "none" | "low" | "medium" | "high" | "unknown";
13
14export interface LicenseInfo {
15 name: string;
16 ecosystem: string;
17 version: string;
18 license: string;
19 risk: LicenseRisk;
20 reason?: string;
21}
22
23export interface LicenseReport {
24 totalDeps: number;
25 scanned: number;
26 risks: {
27 high: LicenseInfo[];
28 medium: LicenseInfo[];
29 low: LicenseInfo[];
30 unknown: LicenseInfo[];
31 };
32 compliant: boolean;
33 summary: string;
34}
35
36const COPYLEFT_HIGH: string[] = [
37 "AGPL-3.0",
38 "AGPL-3.0-only",
39 "AGPL-3.0-or-later",
40 "GPL-3.0",
41 "GPL-3.0-only",
42 "GPL-3.0-or-later",
43 "GPL-2.0",
44 "GPL-2.0-only",
45 "GPL-2.0-or-later",
46 "SSPL-1.0",
47 "EUPL-1.2",
48];
49
50const COPYLEFT_MEDIUM: string[] = [
51 "LGPL-3.0",
52 "LGPL-3.0-only",
53 "LGPL-3.0-or-later",
54 "LGPL-2.1",
55 "LGPL-2.1-only",
56 "LGPL-2.1-or-later",
57 "MPL-2.0",
58 "EPL-2.0",
59 "EPL-1.0",
60 "CDDL-1.0",
61 "OSL-3.0",
62];
63
64const PERMISSIVE: string[] = [
65 "MIT",
66 "Apache-2.0",
67 "BSD-2-Clause",
68 "BSD-3-Clause",
69 "ISC",
70 "0BSD",
71 "Unlicense",
72 "CC0-1.0",
73 "CC-BY-4.0",
74 "CC-BY-3.0",
75 "Zlib",
76 "BSL-1.0",
77 "PSF-2.0",
78 "Python-2.0",
79 "BlueOak-1.0.0",
80];
81
82// Well-known package licenses (avoids registry lookups for the most common packages)
83const KNOWN_LICENSES: Record<string, string> = {
84 "react": "MIT",
85 "react-dom": "MIT",
86 "typescript": "Apache-2.0",
87 "express": "MIT",
88 "lodash": "MIT",
89 "axios": "MIT",
90 "next": "MIT",
91 "vue": "MIT",
92 "angular": "MIT",
93 "hono": "MIT",
94 "drizzle-orm": "Apache-2.0",
95 "esbuild": "MIT",
96 "bun": "MIT",
97 "tailwindcss": "MIT",
98 "postcss": "MIT",
99 "webpack": "MIT",
100 "vite": "MIT",
101 "eslint": "MIT",
102 "prettier": "MIT",
103 "jest": "MIT",
104 "@types/node": "MIT",
105 "@types/react": "MIT",
106 "zod": "MIT",
107 "prisma": "Apache-2.0",
108 "@prisma/client": "Apache-2.0",
109 "pg": "MIT",
110 "mysql2": "MIT",
111 "mongoose": "MIT",
112 "cors": "MIT",
113 "dotenv": "BSD-2-Clause",
114 "chalk": "MIT",
115 "commander": "MIT",
116 "inquirer": "MIT",
117 "glob": "ISC",
118 "rimraf": "ISC",
119 "uuid": "MIT",
120 "moment": "MIT",
121 "dayjs": "MIT",
122 "date-fns": "MIT",
123 "sharp": "Apache-2.0",
124 "bcrypt": "MIT",
125 "jsonwebtoken": "MIT",
126 "socket.io": "MIT",
127 "redis": "MIT",
128 "ioredis": "MIT",
129 "aws-sdk": "Apache-2.0",
130 "@aws-sdk/client-s3": "Apache-2.0",
131 "stripe": "MIT",
132 "nodemailer": "MIT",
133 "highlight.js": "BSD-3-Clause",
134 "marked": "MIT",
135 "@anthropic-ai/sdk": "MIT",
136 "openai": "Apache-2.0",
137 "mysql": "MIT",
138 "sqlite3": "BSD-3-Clause",
139 "better-sqlite3": "MIT",
140 "ffmpeg": "LGPL-2.1",
141 "readline-sync": "MIT",
142 "puppeteer": "Apache-2.0",
143 "playwright": "Apache-2.0",
144};
145
146function classifyLicense(spdxId: string): { risk: LicenseRisk; reason?: string } {
147 const normalized = spdxId.trim();
148
149 if (COPYLEFT_HIGH.some((l) => normalized.includes(l))) {
150 return { risk: "high", reason: `Copyleft license (${normalized}) — may require source disclosure` };
151 }
152 if (COPYLEFT_MEDIUM.some((l) => normalized.includes(l))) {
153 return { risk: "medium", reason: `Weak copyleft (${normalized}) — review linking requirements` };
154 }
155 if (PERMISSIVE.some((l) => normalized.includes(l))) {
156 return { risk: "none" };
157 }
158 return { risk: "low", reason: `Uncommon license (${normalized}) — verify compatibility` };
159}
160
161function lookupLicense(name: string): string | null {
162 return KNOWN_LICENSES[name] ?? null;
163}
164
165export async function scanLicenses(repositoryId: string): Promise<LicenseReport> {
166 const deps = await listDependenciesForRepo(repositoryId);
167
168 const results: LicenseInfo[] = [];
169 const risks: LicenseReport["risks"] = { high: [], medium: [], low: [], unknown: [] };
170
171 for (const dep of deps) {
172 const license = lookupLicense(dep.name);
173 const version = dep.versionSpec?.replace(/^[~^>=<!\s]+/, "") || "unknown";
174
175 if (!license) {
176 const info: LicenseInfo = {
177 name: dep.name,
178 ecosystem: dep.ecosystem,
179 version,
180 license: "UNKNOWN",
181 risk: "unknown",
182 reason: "License not in local database — verify manually",
183 };
184 results.push(info);
185 risks.unknown.push(info);
186 continue;
187 }
188
189 const { risk, reason } = classifyLicense(license);
190 const info: LicenseInfo = {
191 name: dep.name,
192 ecosystem: dep.ecosystem,
193 version,
194 license,
195 risk,
196 reason,
197 };
198 results.push(info);
199
200 if (risk === "high") risks.high.push(info);
201 else if (risk === "medium") risks.medium.push(info);
202 else if (risk === "low") risks.low.push(info);
203 }
204
205 const highCount = risks.high.length;
206 const medCount = risks.medium.length;
207 const unknownCount = risks.unknown.length;
208
209 let summary: string;
210 if (highCount > 0) {
211 summary = `${highCount} high-risk copyleft license${highCount === 1 ? "" : "s"} detected — review required before shipping.`;
212 } else if (medCount > 0) {
213 summary = `${medCount} weak copyleft license${medCount === 1 ? "" : "s"} found — verify linking requirements.`;
214 } else if (unknownCount > 0) {
215 summary = `All known licenses are permissive. ${unknownCount} package${unknownCount === 1 ? "" : "s"} with unknown licenses need manual review.`;
216 } else {
217 summary = "All dependencies use permissive licenses. No compliance issues detected.";
218 }
219
220 return {
221 totalDeps: deps.length,
222 scanned: results.length,
223 risks,
224 compliant: highCount === 0,
225 summary,
226 };
227}