@rio-cloud/rio-license-checker 1.1.2 → 1.1.7
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/dist/index.mjs +232 -0
- package/package.json +18 -19
- package/CHANGELOG.md +0 -34
- package/src/cli.ts +0 -115
- package/src/license-bucket.ts +0 -73
- package/src/license-checker-gradle.ts +0 -70
- package/src/license-checker-npm.ts +0 -38
- package/src/rio-license-checker.ts +0 -19
- package/src/types.ts +0 -25
- package/tsconfig.json +0 -6
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import * as fs$1 from "node:fs";
|
|
4
|
+
import * as path$1 from "node:path";
|
|
5
|
+
import { Command, Option } from "commander";
|
|
6
|
+
import logger from "loglevel";
|
|
7
|
+
import { GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
8
|
+
import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
|
|
9
|
+
import { $, fs, path } from "zx";
|
|
10
|
+
|
|
11
|
+
//#region src/types.ts
|
|
12
|
+
var LicenseCheckError = class extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "LicenseCheckError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var CliArgsError = class extends Error {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "CliArgsError";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
let LicenseCheckType = /* @__PURE__ */ function(LicenseCheckType$1) {
|
|
25
|
+
LicenseCheckType$1["NPM_BACKEND"] = "npm-backend";
|
|
26
|
+
LicenseCheckType$1["NPM_FRONTEND"] = "npm-frontend";
|
|
27
|
+
LicenseCheckType$1["GRADLE"] = "gradle";
|
|
28
|
+
return LicenseCheckType$1;
|
|
29
|
+
}({});
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/license-bucket.ts
|
|
33
|
+
let ossLicensesBucketName;
|
|
34
|
+
const OSS_LICENSES_BUCKET_NAME_SSM_PARAMETER = "/config/oss-licenses/bucket-name";
|
|
35
|
+
const getOssLicensesBucketName = async () => {
|
|
36
|
+
if (!ossLicensesBucketName) {
|
|
37
|
+
const ssmClient = new SSMClient();
|
|
38
|
+
const command = new GetParameterCommand({ Name: OSS_LICENSES_BUCKET_NAME_SSM_PARAMETER });
|
|
39
|
+
const response = await ssmClient.send(command);
|
|
40
|
+
if (!response.Parameter?.Value) throw new Error("No license bucket name found");
|
|
41
|
+
ossLicensesBucketName = response.Parameter.Value;
|
|
42
|
+
logger.debug(`OSS Licenses Bucket: ${ossLicensesBucketName}`);
|
|
43
|
+
}
|
|
44
|
+
return ossLicensesBucketName;
|
|
45
|
+
};
|
|
46
|
+
const getWhitelistKey = (checkType) => {
|
|
47
|
+
switch (checkType) {
|
|
48
|
+
case LicenseCheckType.NPM_FRONTEND: return "whitelist-npm-frontend.txt";
|
|
49
|
+
case LicenseCheckType.NPM_BACKEND: return "whitelist-npm-backend.txt";
|
|
50
|
+
case LicenseCheckType.GRADLE: return "whitelist-gradle.txt";
|
|
51
|
+
default: throw new Error(`Unknown whitelist type: ${checkType}`);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const downloadWhitelist = async (checkType) => {
|
|
55
|
+
const getWhitelistObject = new GetObjectCommand({
|
|
56
|
+
Bucket: await getOssLicensesBucketName(),
|
|
57
|
+
Key: getWhitelistKey(checkType)
|
|
58
|
+
});
|
|
59
|
+
const whitelistResponse = await new S3Client().send(getWhitelistObject);
|
|
60
|
+
if (!whitelistResponse.Body) throw new Error("No whitelist found");
|
|
61
|
+
return (await whitelistResponse.Body.transformToString()).split("\n").map((line) => stripQuotes(line)).filter((line) => line.trim().length > 0);
|
|
62
|
+
};
|
|
63
|
+
const stripQuotes = (line) => {
|
|
64
|
+
if (line.startsWith("\"") && line.endsWith("\"")) return line.slice(1, -1);
|
|
65
|
+
return line;
|
|
66
|
+
};
|
|
67
|
+
const uploadLicenseReport = async (accountName, serviceName, type, report) => {
|
|
68
|
+
const s3Client = new S3Client();
|
|
69
|
+
const putObjectCommand = new PutObjectCommand({
|
|
70
|
+
Bucket: await getOssLicensesBucketName(),
|
|
71
|
+
Key: `reports/${accountName}/${serviceName}_${type}.txt`,
|
|
72
|
+
Body: report
|
|
73
|
+
});
|
|
74
|
+
await s3Client.send(putObjectCommand);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/license-checker-gradle.ts
|
|
79
|
+
const normalizeLicenseReport = (report) => {
|
|
80
|
+
const sortedLicences = report.licences.map((license) => ({
|
|
81
|
+
...license,
|
|
82
|
+
dependencies: [...license.dependencies].sort((a, b) => a.localeCompare(b))
|
|
83
|
+
})).sort((a, b) => {
|
|
84
|
+
const nameCompare = a.name.localeCompare(b.name);
|
|
85
|
+
if (nameCompare !== 0) return nameCompare;
|
|
86
|
+
const urlCompare = (a.url ?? "").localeCompare(b.url ?? "");
|
|
87
|
+
if (urlCompare !== 0) return urlCompare;
|
|
88
|
+
const dependenciesA = a.dependencies.join("|");
|
|
89
|
+
const dependenciesB = b.dependencies.join("|");
|
|
90
|
+
return dependenciesA.localeCompare(dependenciesB);
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
...report,
|
|
94
|
+
licences: sortedLicences
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Calls the gradle license checker with the given options. If a license is found that is not in the whitelist, an error is thrown.
|
|
99
|
+
* @param directory Folder containing the package.json to analyze
|
|
100
|
+
* @param licenseWhitelist List of allowed licenses
|
|
101
|
+
* @param excludePackages List of packages to exclude from the report, e.g. the current package
|
|
102
|
+
*
|
|
103
|
+
* @returns A dependency report generated by the license checker
|
|
104
|
+
*/
|
|
105
|
+
const callGradleLicenseChecker = async ({ directory, licenseWhitelist }) => {
|
|
106
|
+
const gradleWrapper = await discoverGradleWrapper(directory);
|
|
107
|
+
const result = await $({
|
|
108
|
+
cwd: directory,
|
|
109
|
+
nothrow: true,
|
|
110
|
+
quiet: true
|
|
111
|
+
})`./${gradleWrapper} downloadLicenses`;
|
|
112
|
+
if (result.exitCode !== 0) throw new LicenseCheckError(`NOTE: This script requires the license-gradle-plugin to be configured, so that the script ./gradlew downloadLicenses works.\nSee https://github.com/hierynomus/license-gradle-plugin for details.\n${result.stderr.trimEnd()}`);
|
|
113
|
+
const licensesByLicenseNameFile = path.join(directory, "build", "reports", "license", "license-dependency.json");
|
|
114
|
+
const licensesByLicenseName = normalizeLicenseReport(JSON.parse(await fs.readFile(licensesByLicenseNameFile, "utf8")));
|
|
115
|
+
if (licensesByLicenseName.licences.length === 0) throw new LicenseCheckError("List of licenses is empty. It is highly unlikely your project has no dependencies. Check the configuration of the license-gradle-plugin.");
|
|
116
|
+
const allowedLicenses = new Set(licenseWhitelist);
|
|
117
|
+
const notAllowedLicenses = /* @__PURE__ */ new Set();
|
|
118
|
+
for (const license of licensesByLicenseName.licences) if (!allowedLicenses.has(license.name)) {
|
|
119
|
+
notAllowedLicenses.add(license.name);
|
|
120
|
+
logger.warn(`License "${license.name}" is not in the whitelist. It is used by:\n${license.dependencies.join("\n")}`);
|
|
121
|
+
}
|
|
122
|
+
if (notAllowedLicenses.size > 0) throw new LicenseCheckError(`The following licenses are not in the whitelist: ${Array.from(notAllowedLicenses).sort().join(", ")}`);
|
|
123
|
+
return JSON.stringify(licensesByLicenseName, null, 2);
|
|
124
|
+
};
|
|
125
|
+
const discoverGradleWrapper = async (directory) => {
|
|
126
|
+
const gradleWrapperFile = path.join(directory, "gradlew");
|
|
127
|
+
if (fs.existsSync(gradleWrapperFile)) return "gradlew";
|
|
128
|
+
if (fs.existsSync(path.join(directory, "..", "gradlew"))) return "../gradlew";
|
|
129
|
+
throw new LicenseCheckError(`Could not find gradle wrapper in ${directory} or the parent directory. Please ensure that the gradle wrapper is present.`);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/license-checker-npm.ts
|
|
134
|
+
const resolveLicenseCheckerPath = () => {
|
|
135
|
+
return createRequire(import.meta.url).resolve("license-checker-rseidelsohn/bin/license-checker-rseidelsohn");
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Calls the npm license checker with the given options. If a license is found that is not in the whitelist, an error is thrown.
|
|
139
|
+
* @param directory Folder containing the package.json to analyze
|
|
140
|
+
* @param licenseWhitelist List of allowed licenses
|
|
141
|
+
* @param excludePackages List of packages to exclude from the report, e.g. the current package
|
|
142
|
+
*
|
|
143
|
+
* @returns A dependency report generated by the license checker
|
|
144
|
+
*/
|
|
145
|
+
const callNpmLicenseChecker = async ({ directory, licenseWhitelist, excludePackages }) => {
|
|
146
|
+
const licenseCheckExecutable = resolveLicenseCheckerPath();
|
|
147
|
+
if (licenseWhitelist.length === 0) throw new Error("No licenses in whitelist. This would allow all licenses, which is certainly not intended.");
|
|
148
|
+
const onlyAllowOption = licenseWhitelist.join(";");
|
|
149
|
+
const excludePackagesOption = excludePackages.join(";");
|
|
150
|
+
const result = await $({
|
|
151
|
+
cwd: directory,
|
|
152
|
+
nothrow: true,
|
|
153
|
+
quiet: true
|
|
154
|
+
})`${licenseCheckExecutable} --json --production --onlyAllow ${onlyAllowOption} --excludePackages ${excludePackagesOption} --relativeModulePath`;
|
|
155
|
+
if (result.exitCode !== 0) throw new LicenseCheckError(result.stderr.trimEnd());
|
|
156
|
+
return result.stdout;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/cli.ts
|
|
161
|
+
const cli = async (args) => {
|
|
162
|
+
const program = new Command();
|
|
163
|
+
program.name("check-licenses").option("-a, --account-name <account>", "Account / context name").option("-s, --service-name <service>", "Service name").addOption(new Option("-t, --type <type>", "Type of the license check").makeOptionMandatory(true).choices(Object.values(LicenseCheckType))).addOption(new Option("-d, --directory <directory>", "Directory to check licenses for (defaults to current directory)").default(process.cwd())).addOption(new Option("-u, --upload", "Upload license report to S3").default(false)).addOption(new Option("-v, --verbose", "Enable debug logging").default(false)).parse(args);
|
|
164
|
+
logger.setLevel(program.opts().verbose ? "debug" : "info");
|
|
165
|
+
const contextName = program.opts().accountName;
|
|
166
|
+
const serviceName = program.opts().serviceName;
|
|
167
|
+
const checkType = parseType(program.opts().type);
|
|
168
|
+
const upload = program.opts().upload;
|
|
169
|
+
if (!contextName) {
|
|
170
|
+
if (upload) throw new CliArgsError("Account name must not be empty when --upload is specified");
|
|
171
|
+
} else if (!contextName.match(/^[a-zA-Z0-9-_]+$/)) throw new CliArgsError("Account name must only contain alphanumeric characters, hyphens and underscores");
|
|
172
|
+
if (!serviceName) {
|
|
173
|
+
if (upload) throw new CliArgsError("Service name must not be empty when --upload is specified");
|
|
174
|
+
} else if (!serviceName.match(/^[a-zA-Z0-9-_]+$/)) throw new CliArgsError("Service name must only contain alphanumeric characters, hyphens and underscores");
|
|
175
|
+
const licenseWhitelist = await downloadWhitelist(checkType);
|
|
176
|
+
logger.debug(`Licenses whitelist:\n----------------\n${licenseWhitelist.join("\n")}\n----------------`);
|
|
177
|
+
let result;
|
|
178
|
+
if ([LicenseCheckType.NPM_BACKEND, LicenseCheckType.NPM_FRONTEND].includes(checkType)) {
|
|
179
|
+
if (!fs$1.existsSync(path$1.join(program.opts().directory, "node_modules"))) throw new Error("No node_modules directory found. Please run \"npm ci\" before using the license checker.");
|
|
180
|
+
const excludeForApplicationPackage = getExcludeForApplicationPackage(program.opts().directory);
|
|
181
|
+
const ignorePackages = readIgnorePackages(program.opts().directory);
|
|
182
|
+
result = await callNpmLicenseChecker({
|
|
183
|
+
directory: program.opts().directory,
|
|
184
|
+
licenseWhitelist,
|
|
185
|
+
excludePackages: [excludeForApplicationPackage, ...ignorePackages]
|
|
186
|
+
});
|
|
187
|
+
} else if (checkType === LicenseCheckType.GRADLE) result = await callGradleLicenseChecker({
|
|
188
|
+
directory: program.opts().directory,
|
|
189
|
+
licenseWhitelist
|
|
190
|
+
});
|
|
191
|
+
else throw new Error(`Unknown license check type: ${program.opts().type}`);
|
|
192
|
+
logger.debug(`License report:\n${result.trimEnd()}`);
|
|
193
|
+
if (upload) {
|
|
194
|
+
await uploadLicenseReport(contextName, serviceName, checkType, result);
|
|
195
|
+
logger.info(`Uploaded license report for ${contextName}/${serviceName}_${checkType}`);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const parseType = (value) => {
|
|
199
|
+
for (const type of Object.values(LicenseCheckType)) if (type === value) return type;
|
|
200
|
+
throw new Error(`Unknown whitelist type: ${value}`);
|
|
201
|
+
};
|
|
202
|
+
const getExcludeForApplicationPackage = (directory) => {
|
|
203
|
+
const packageJson = JSON.parse(fs$1.readFileSync(path$1.join(directory, "package.json"), "utf8"));
|
|
204
|
+
const serviceVersion = packageJson.version;
|
|
205
|
+
return `${packageJson.name}@${serviceVersion}`;
|
|
206
|
+
};
|
|
207
|
+
const readIgnorePackages = (directory) => {
|
|
208
|
+
const ignorePackagesFile = path$1.join(directory, "oss-licenses-ignore-packages.txt");
|
|
209
|
+
let ignorePackages = [];
|
|
210
|
+
if (fs$1.existsSync(ignorePackagesFile)) ignorePackages = fs$1.readFileSync(ignorePackagesFile, "utf8").split("\n").filter((line) => line.trim() !== "");
|
|
211
|
+
return ignorePackages;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/index.ts
|
|
216
|
+
try {
|
|
217
|
+
await cli(process.argv);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (error instanceof LicenseCheckError) {
|
|
220
|
+
console.error(error.message);
|
|
221
|
+
process.exit(2);
|
|
222
|
+
} else if (error instanceof CliArgsError) {
|
|
223
|
+
console.error(error.message);
|
|
224
|
+
process.exit(3);
|
|
225
|
+
} else {
|
|
226
|
+
console.error(error);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
//#endregion
|
|
232
|
+
export { };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rio-cloud/rio-license-checker",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
"organization": true
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
|
+
"build": "tsdown",
|
|
16
17
|
"pretest": "tsc",
|
|
17
|
-
"test": "vitest --test-timeout=60000",
|
|
18
|
+
"test": "vitest run --test-timeout=60000",
|
|
18
19
|
"vulnerability-check": "npm audit --registry https://registry.npmjs.org --parseable --audit-level=moderate --omit=dev",
|
|
19
20
|
"bump": "commit-and-tag-version -a --no-verify",
|
|
20
21
|
"release": "npm run test && npm run bump",
|
|
@@ -23,39 +24,37 @@
|
|
|
23
24
|
"release:push": "echo '✅ pushing release' && git push origin main --follow-tags"
|
|
24
25
|
},
|
|
25
26
|
"bin": {
|
|
26
|
-
"rio-license-checker": "
|
|
27
|
+
"rio-license-checker": "dist/index.mjs"
|
|
27
28
|
},
|
|
28
29
|
"files": [
|
|
29
|
-
"
|
|
30
|
-
"tsconfig.json",
|
|
31
|
-
"README.md",
|
|
32
|
-
"CHANGELOG.md"
|
|
30
|
+
"dist"
|
|
33
31
|
],
|
|
34
32
|
"dependencies": {
|
|
35
|
-
"@aws-sdk/client-s3": "3.
|
|
36
|
-
"@aws-sdk/client-ssm": "3.
|
|
37
|
-
"commander": "14.0.
|
|
33
|
+
"@aws-sdk/client-s3": "3.932.0",
|
|
34
|
+
"@aws-sdk/client-ssm": "3.932.0",
|
|
35
|
+
"commander": "14.0.2",
|
|
38
36
|
"license-checker-rseidelsohn": "4.4.2",
|
|
39
37
|
"loglevel": "1.9.2",
|
|
40
|
-
"
|
|
41
|
-
"zx": "8.8.1"
|
|
38
|
+
"zx": "8.8.5"
|
|
42
39
|
},
|
|
43
40
|
"devDependencies": {
|
|
44
|
-
"@biomejs/biome": "2.2.
|
|
41
|
+
"@biomejs/biome": "2.2.6",
|
|
45
42
|
"@rio-cloud/biome-config-claid": "1.0.0",
|
|
46
|
-
"@tsconfig/node22": "22.0.
|
|
43
|
+
"@tsconfig/node22": "22.0.5",
|
|
47
44
|
"@types/jest": "30.0.0",
|
|
48
45
|
"@types/license-checker": "25.0.6",
|
|
49
|
-
"@types/node": "24.
|
|
46
|
+
"@types/node": "24.10.1",
|
|
50
47
|
"aws-sdk-client-mock": "4.1.0",
|
|
51
48
|
"aws-sdk-client-mock-jest": "4.1.0",
|
|
52
|
-
"chalk": "5.6.
|
|
53
|
-
"commit-and-tag-version": "12.
|
|
54
|
-
"
|
|
49
|
+
"chalk": "5.6.2",
|
|
50
|
+
"commit-and-tag-version": "12.6.0",
|
|
51
|
+
"tsdown": "0.16.5",
|
|
52
|
+
"typescript": "5.9.3",
|
|
55
53
|
"vitest": "3.2.4"
|
|
56
54
|
},
|
|
57
55
|
"overrides": {
|
|
58
|
-
"form-data": ">=4.0.4"
|
|
56
|
+
"form-data": ">=4.0.4",
|
|
57
|
+
"glob": ">=10.5.0"
|
|
59
58
|
},
|
|
60
59
|
"commit-and-tag-version": {
|
|
61
60
|
"commitUrlFormat": "https://bitbucket.collaboration-man.com/projects/RIODEV/repos/rio-license-checker/commits/{{hash}}",
|
package/CHANGELOG.md
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
|
4
|
-
|
|
5
|
-
## [1.1.2](https://bitbucket.collaboration-man.com/projects/RIODEV/repos/rio-license-checker/compare/commits?targetBranch=refs%2Ftags%2Fv1.1.1&sourceBranch=refs%2Ftags%2Fv1.1.2) (2025-09-08)
|
|
6
|
-
|
|
7
|
-
## [1.1.1](https://bitbucket.collaboration-man.com/projects/RIODEV/repos/rio-license-checker/compare/commits?targetBranch=refs%2Ftags%2Fv1.1.0&sourceBranch=refs%2Ftags%2Fv1.1.1) (2025-06-03)
|
|
8
|
-
|
|
9
|
-
## [1.1.0](https://bitbucket.collaboration-man.com/projects/RIODEV/repos/rio-license-checker/compare/commits?targetBranch=refs%2Ftags%2Fv1.0.1&sourceBranch=refs%2Ftags%2Fv1.1.0) (2025-06-03)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
### Features
|
|
13
|
-
|
|
14
|
-
* Support gradle subprojects ([0cee7bf](https://bitbucket.collaboration-man.com/projects/RIODEV/repos/rio-license-checker/commits/0cee7bfc2c2bde766fd5aaa329dc592c53d89de7))
|
|
15
|
-
|
|
16
|
-
## [1.0.1](https://bitbucket.collaboration-man.com/projects/RIODEV/repos/rio-license-checker/compare/commits?targetBranch=refs%2Ftags%2Fv1.0.0&sourceBranch=refs%2Ftags%2Fv1.0.1) (2025-05-20)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
### Bug Fixes
|
|
20
|
-
|
|
21
|
-
* relax required node version ([c8a3d8d](https://bitbucket.collaboration-man.com/projects/RIODEV/repos/rio-license-checker/commits/c8a3d8de0a2da634a7e869b97932d9be0e6bc36c))
|
|
22
|
-
|
|
23
|
-
## [1.0.0](https://bitbucket.collaboration-man.com/projects/RIODEV/repos/rio-license-checker/compare/commits?targetBranch=refs%2Ftags%2Fv1.0.0-alpha.2&sourceBranch=refs%2Ftags%2Fv1.0.0) (2025-03-28)
|
|
24
|
-
|
|
25
|
-
## [1.0.0-alpha.2](https://bitbucket.collaboration-man.com/projects/RIODEV/repos/rio-license-checker/compare/commits?targetBranch=refs%2Ftags%2Fv1.0.0-alpha.1&sourceBranch=refs%2Ftags%2Fv1.0.0-alpha.2) (2025-03-21)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
### Features
|
|
29
|
-
|
|
30
|
-
* Append type (passed via --type) as suffix to the uploaded filename ([382362b](https://bitbucket.collaboration-man.com/projects/RIODEV/repos/rio-license-checker/commits/382362ba1afdfaa9f758cf156f01c392f60ebea1))
|
|
31
|
-
|
|
32
|
-
## [1.0.0-alpha.1](https://bitbucket.collaboration-man.com/projects/RIODEV/repos/rio-license-checker/compare/commits?targetBranch=refs%2Ftags%2Fv1.0.0-alpha.0&sourceBranch=refs%2Ftags%2Fv1.0.0-alpha.1) (2025-03-13)
|
|
33
|
-
|
|
34
|
-
## 1.0.0-alpha.0 (2025-03-12)
|
package/src/cli.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { Command, Option } from 'commander';
|
|
2
|
-
import * as fs from 'node:fs';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
|
-
import { downloadWhitelist, uploadLicenseReport } from './license-bucket.js';
|
|
5
|
-
import logger from 'loglevel';
|
|
6
|
-
import { callNpmLicenseChecker } from './license-checker-npm.js';
|
|
7
|
-
import { callGradleLicenseChecker } from './license-checker-gradle.js';
|
|
8
|
-
import { CliArgsError, LicenseCheckType } from './types.js';
|
|
9
|
-
|
|
10
|
-
export const cli = async (args: string[]) => {
|
|
11
|
-
const program = new Command();
|
|
12
|
-
|
|
13
|
-
program
|
|
14
|
-
.name('check-licenses')
|
|
15
|
-
.option('-a, --account-name <account>', 'Account / context name')
|
|
16
|
-
.option('-s, --service-name <service>', 'Service name')
|
|
17
|
-
.addOption(
|
|
18
|
-
new Option('-t, --type <type>', 'Type of the license check')
|
|
19
|
-
.makeOptionMandatory(true)
|
|
20
|
-
.choices(Object.values(LicenseCheckType)),
|
|
21
|
-
)
|
|
22
|
-
.addOption(
|
|
23
|
-
new Option(
|
|
24
|
-
'-d, --directory <directory>',
|
|
25
|
-
'Directory to check licenses for (defaults to current directory)',
|
|
26
|
-
).default(process.cwd()),
|
|
27
|
-
)
|
|
28
|
-
.addOption(new Option('-u, --upload', 'Upload license report to S3').default(false))
|
|
29
|
-
.addOption(new Option('-v, --verbose', 'Enable debug logging').default(false))
|
|
30
|
-
.parse(args);
|
|
31
|
-
|
|
32
|
-
logger.setLevel(program.opts().verbose ? 'debug' : 'info');
|
|
33
|
-
|
|
34
|
-
const contextName = program.opts().accountName;
|
|
35
|
-
const serviceName = program.opts().serviceName;
|
|
36
|
-
const checkType = parseType(program.opts().type);
|
|
37
|
-
const upload = program.opts().upload as boolean;
|
|
38
|
-
|
|
39
|
-
if (!contextName) {
|
|
40
|
-
if (upload) {
|
|
41
|
-
throw new CliArgsError('Account name must not be empty when --upload is specified');
|
|
42
|
-
}
|
|
43
|
-
} else if (!contextName.match(/^[a-zA-Z0-9-_]+$/)) {
|
|
44
|
-
throw new CliArgsError('Account name must only contain alphanumeric characters, hyphens and underscores');
|
|
45
|
-
}
|
|
46
|
-
if (!serviceName) {
|
|
47
|
-
if (upload) {
|
|
48
|
-
throw new CliArgsError('Service name must not be empty when --upload is specified');
|
|
49
|
-
}
|
|
50
|
-
} else if (!serviceName.match(/^[a-zA-Z0-9-_]+$/)) {
|
|
51
|
-
throw new CliArgsError('Service name must only contain alphanumeric characters, hyphens and underscores');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const licenseWhitelist = await downloadWhitelist(checkType);
|
|
55
|
-
logger.debug(`Licenses whitelist:\n----------------\n${licenseWhitelist.join('\n')}\n----------------`);
|
|
56
|
-
|
|
57
|
-
let result: string;
|
|
58
|
-
|
|
59
|
-
if ([LicenseCheckType.NPM_BACKEND, LicenseCheckType.NPM_FRONTEND].includes(checkType)) {
|
|
60
|
-
if (!fs.existsSync(path.join(program.opts().directory, 'node_modules'))) {
|
|
61
|
-
throw new Error('No node_modules directory found. Please run "npm ci" before using the license checker.');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const excludeForApplicationPackage = getExcludeForApplicationPackage(program.opts().directory);
|
|
65
|
-
const ignorePackages = readIgnorePackages(program.opts().directory);
|
|
66
|
-
|
|
67
|
-
result = await callNpmLicenseChecker({
|
|
68
|
-
directory: program.opts().directory,
|
|
69
|
-
licenseWhitelist,
|
|
70
|
-
excludePackages: [excludeForApplicationPackage, ...ignorePackages],
|
|
71
|
-
});
|
|
72
|
-
} else if (checkType === LicenseCheckType.GRADLE) {
|
|
73
|
-
result = await callGradleLicenseChecker({
|
|
74
|
-
directory: program.opts().directory,
|
|
75
|
-
licenseWhitelist,
|
|
76
|
-
});
|
|
77
|
-
} else {
|
|
78
|
-
throw new Error(`Unknown license check type: ${program.opts().type}`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
logger.debug(`License report:\n${result.trimEnd()}`);
|
|
82
|
-
|
|
83
|
-
if (upload) {
|
|
84
|
-
await uploadLicenseReport(contextName, serviceName, checkType, result);
|
|
85
|
-
logger.info(`Uploaded license report for ${contextName}/${serviceName}_${checkType}`);
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
export const parseType = (value: string): LicenseCheckType => {
|
|
90
|
-
for (const type of Object.values(LicenseCheckType)) {
|
|
91
|
-
if (type === value) {
|
|
92
|
-
return type;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
throw new Error(`Unknown whitelist type: ${value}`);
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const getExcludeForApplicationPackage = (directory: string): string => {
|
|
99
|
-
const packageJson = JSON.parse(fs.readFileSync(path.join(directory, 'package.json'), 'utf8'));
|
|
100
|
-
const serviceVersion = packageJson.version;
|
|
101
|
-
const packageName = packageJson.name;
|
|
102
|
-
return `${packageName}@${serviceVersion}`;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const readIgnorePackages = (directory: string): string[] => {
|
|
106
|
-
const ignorePackagesFile = path.join(directory, 'oss-licenses-ignore-packages.txt');
|
|
107
|
-
let ignorePackages: string[] = [];
|
|
108
|
-
if (fs.existsSync(ignorePackagesFile)) {
|
|
109
|
-
ignorePackages = fs
|
|
110
|
-
.readFileSync(ignorePackagesFile, 'utf8')
|
|
111
|
-
.split('\n')
|
|
112
|
-
.filter((line) => line.trim() !== '');
|
|
113
|
-
}
|
|
114
|
-
return ignorePackages;
|
|
115
|
-
};
|
package/src/license-bucket.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm';
|
|
2
|
-
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
|
3
|
-
import logger from 'loglevel';
|
|
4
|
-
import { LicenseCheckType } from './types.js';
|
|
5
|
-
|
|
6
|
-
let ossLicensesBucketName: string;
|
|
7
|
-
|
|
8
|
-
const OSS_LICENSES_BUCKET_NAME_SSM_PARAMETER = '/config/oss-licenses/bucket-name';
|
|
9
|
-
|
|
10
|
-
const getOssLicensesBucketName = async (): Promise<string> => {
|
|
11
|
-
if (!ossLicensesBucketName) {
|
|
12
|
-
const ssmClient = new SSMClient();
|
|
13
|
-
const command = new GetParameterCommand({
|
|
14
|
-
Name: OSS_LICENSES_BUCKET_NAME_SSM_PARAMETER,
|
|
15
|
-
});
|
|
16
|
-
const response = await ssmClient.send(command);
|
|
17
|
-
if (!response.Parameter?.Value) {
|
|
18
|
-
throw new Error('No license bucket name found');
|
|
19
|
-
}
|
|
20
|
-
ossLicensesBucketName = response.Parameter.Value;
|
|
21
|
-
logger.debug(`OSS Licenses Bucket: ${ossLicensesBucketName}`);
|
|
22
|
-
}
|
|
23
|
-
return ossLicensesBucketName;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const getWhitelistKey = (checkType: LicenseCheckType): string => {
|
|
27
|
-
switch (checkType) {
|
|
28
|
-
case LicenseCheckType.NPM_FRONTEND:
|
|
29
|
-
return 'whitelist-npm-frontend.txt';
|
|
30
|
-
case LicenseCheckType.NPM_BACKEND:
|
|
31
|
-
return 'whitelist-npm-backend.txt';
|
|
32
|
-
case LicenseCheckType.GRADLE:
|
|
33
|
-
return 'whitelist-gradle.txt';
|
|
34
|
-
default:
|
|
35
|
-
throw new Error(`Unknown whitelist type: ${checkType}`);
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export const downloadWhitelist = async (checkType: LicenseCheckType): Promise<string[]> => {
|
|
40
|
-
const getWhitelistObject = new GetObjectCommand({
|
|
41
|
-
Bucket: await getOssLicensesBucketName(),
|
|
42
|
-
Key: getWhitelistKey(checkType),
|
|
43
|
-
});
|
|
44
|
-
const s3Client = new S3Client();
|
|
45
|
-
|
|
46
|
-
const whitelistResponse = await s3Client.send(getWhitelistObject);
|
|
47
|
-
if (!whitelistResponse.Body) {
|
|
48
|
-
throw new Error('No whitelist found');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const whitelistBuffer = await whitelistResponse.Body.transformToString();
|
|
52
|
-
return whitelistBuffer
|
|
53
|
-
.split('\n')
|
|
54
|
-
.map((line) => stripQuotes(line))
|
|
55
|
-
.filter((line) => line.trim().length > 0);
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const stripQuotes = (line: string) => {
|
|
59
|
-
if (line.startsWith('"') && line.endsWith('"')) {
|
|
60
|
-
return line.slice(1, -1);
|
|
61
|
-
}
|
|
62
|
-
return line;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export const uploadLicenseReport = async (accountName: string, serviceName: string, type: string, report: string) => {
|
|
66
|
-
const s3Client = new S3Client();
|
|
67
|
-
const putObjectCommand = new PutObjectCommand({
|
|
68
|
-
Bucket: await getOssLicensesBucketName(),
|
|
69
|
-
Key: `reports/${accountName}/${serviceName}_${type}.txt`,
|
|
70
|
-
Body: report,
|
|
71
|
-
});
|
|
72
|
-
await s3Client.send(putObjectCommand);
|
|
73
|
-
};
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import {$, fs, path} from 'zx';
|
|
2
|
-
import {type LicenseCheckerOptions, LicenseCheckError} from './types.js';
|
|
3
|
-
import logger from 'loglevel';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Calls the gradle license checker with the given options. If a license is found that is not in the whitelist, an error is thrown.
|
|
7
|
-
* @param directory Folder containing the package.json to analyze
|
|
8
|
-
* @param licenseWhitelist List of allowed licenses
|
|
9
|
-
* @param excludePackages List of packages to exclude from the report, e.g. the current package
|
|
10
|
-
*
|
|
11
|
-
* @returns A dependency report generated by the license checker
|
|
12
|
-
*/
|
|
13
|
-
export const callGradleLicenseChecker = async ({
|
|
14
|
-
directory,
|
|
15
|
-
licenseWhitelist,
|
|
16
|
-
}: Omit<LicenseCheckerOptions, 'excludePackages'>): Promise<string> => {
|
|
17
|
-
const gradleWrapper = await discoverGradleWrapper(directory);
|
|
18
|
-
const $$ = $({cwd: directory, nothrow: true, quiet: true});
|
|
19
|
-
const result = await $$`./${gradleWrapper} downloadLicenses`;
|
|
20
|
-
|
|
21
|
-
if (result.exitCode !== 0) {
|
|
22
|
-
throw new LicenseCheckError(
|
|
23
|
-
`NOTE: This script requires the license-gradle-plugin to be configured, so that the script ./gradlew downloadLicenses works.\nSee https://github.com/hierynomus/license-gradle-plugin for details.\n${result.stderr.trimEnd()}`,
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// open file
|
|
28
|
-
const licensesByLicenseNameFile = path.join(directory, 'build', 'reports', 'license', 'license-dependency.json');
|
|
29
|
-
const licensesByLicenseName = JSON.parse(await fs.readFile(licensesByLicenseNameFile, 'utf8'));
|
|
30
|
-
|
|
31
|
-
if (licensesByLicenseName.licences.length === 0) {
|
|
32
|
-
throw new LicenseCheckError(
|
|
33
|
-
'List of licenses is empty. It is highly unlikely your project has no dependencies. Check the configuration of the license-gradle-plugin.',
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// filter out allowed licenses
|
|
38
|
-
const allowedLicenses = new Set(licenseWhitelist);
|
|
39
|
-
const notAllowedLicenses = new Set();
|
|
40
|
-
for (const license of licensesByLicenseName.licences) {
|
|
41
|
-
if (!allowedLicenses.has(license.name)) {
|
|
42
|
-
notAllowedLicenses.add(license.name);
|
|
43
|
-
logger.warn(
|
|
44
|
-
`License "${license.name}" is not in the whitelist. It is used by:\n${license.dependencies.join('\n')}`,
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (notAllowedLicenses.size > 0) {
|
|
50
|
-
throw new LicenseCheckError(
|
|
51
|
-
`The following licenses are not in the whitelist: ${Array.from(notAllowedLicenses).join(', ')}`,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return JSON.stringify(licensesByLicenseName, null, 2);
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const discoverGradleWrapper = async (directory: string): Promise<string | undefined> => {
|
|
59
|
-
// For now, only check the current directory and the parent directory for the gradle wrapper - this might need to be extended if someone has a more complex project structure.
|
|
60
|
-
const gradleWrapperFile = path.join(directory, 'gradlew');
|
|
61
|
-
if (fs.existsSync(gradleWrapperFile)) {
|
|
62
|
-
return 'gradlew';
|
|
63
|
-
}
|
|
64
|
-
if (fs.existsSync(path.join(directory, '..', 'gradlew'))) {
|
|
65
|
-
return '../gradlew';
|
|
66
|
-
}
|
|
67
|
-
throw new LicenseCheckError(
|
|
68
|
-
`Could not find gradle wrapper in ${directory} or the parent directory. Please ensure that the gradle wrapper is present.`,
|
|
69
|
-
);
|
|
70
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { $ } from 'zx';
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
|
-
import { type LicenseCheckerOptions, LicenseCheckError } from './types.js';
|
|
4
|
-
|
|
5
|
-
const resolveLicenseCheckerPath = (): string => {
|
|
6
|
-
const require = createRequire(import.meta.url);
|
|
7
|
-
return require.resolve('license-checker-rseidelsohn/bin/license-checker-rseidelsohn');
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Calls the npm license checker with the given options. If a license is found that is not in the whitelist, an error is thrown.
|
|
12
|
-
* @param directory Folder containing the package.json to analyze
|
|
13
|
-
* @param licenseWhitelist List of allowed licenses
|
|
14
|
-
* @param excludePackages List of packages to exclude from the report, e.g. the current package
|
|
15
|
-
*
|
|
16
|
-
* @returns A dependency report generated by the license checker
|
|
17
|
-
*/
|
|
18
|
-
export const callNpmLicenseChecker = async ({
|
|
19
|
-
directory,
|
|
20
|
-
licenseWhitelist,
|
|
21
|
-
excludePackages,
|
|
22
|
-
}: LicenseCheckerOptions): Promise<string> => {
|
|
23
|
-
const licenseCheckExecutable = resolveLicenseCheckerPath();
|
|
24
|
-
if (licenseWhitelist.length === 0) {
|
|
25
|
-
throw new Error('No licenses in whitelist. This would allow all licenses, which is certainly not intended.');
|
|
26
|
-
}
|
|
27
|
-
const onlyAllowOption = licenseWhitelist.join(';');
|
|
28
|
-
const excludePackagesOption = excludePackages.join(';');
|
|
29
|
-
|
|
30
|
-
const $$ = $({ cwd: directory, nothrow: true, quiet: true });
|
|
31
|
-
const result =
|
|
32
|
-
await $$`${licenseCheckExecutable} --json --production --onlyAllow ${onlyAllowOption} --excludePackages ${excludePackagesOption} --relativeModulePath`;
|
|
33
|
-
if (result.exitCode !== 0) {
|
|
34
|
-
throw new LicenseCheckError(result.stderr.trimEnd());
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return result.stdout;
|
|
38
|
-
};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S npx tsx
|
|
2
|
-
|
|
3
|
-
import { cli } from './cli.js';
|
|
4
|
-
import { CliArgsError, LicenseCheckError } from './types.js';
|
|
5
|
-
|
|
6
|
-
try {
|
|
7
|
-
await cli(process.argv);
|
|
8
|
-
} catch (error) {
|
|
9
|
-
if (error instanceof LicenseCheckError) {
|
|
10
|
-
console.error(error.message);
|
|
11
|
-
process.exit(2);
|
|
12
|
-
} else if (error instanceof CliArgsError) {
|
|
13
|
-
console.error(error.message);
|
|
14
|
-
process.exit(3);
|
|
15
|
-
} else {
|
|
16
|
-
console.error(error);
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export interface LicenseCheckerOptions {
|
|
2
|
-
directory: string;
|
|
3
|
-
licenseWhitelist: string[];
|
|
4
|
-
excludePackages: string[];
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export class LicenseCheckError extends Error {
|
|
8
|
-
constructor(message: string) {
|
|
9
|
-
super(message);
|
|
10
|
-
this.name = 'LicenseCheckError';
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class CliArgsError extends Error {
|
|
15
|
-
constructor(message: string) {
|
|
16
|
-
super(message);
|
|
17
|
-
this.name = 'CliArgsError';
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export enum LicenseCheckType {
|
|
22
|
-
NPM_BACKEND = 'npm-backend',
|
|
23
|
-
NPM_FRONTEND = 'npm-frontend',
|
|
24
|
-
GRADLE = 'gradle',
|
|
25
|
-
}
|