@rio-cloud/rio-license-checker 1.1.2 → 1.2.0-alpha.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.
- package/README.md +65 -30
- package/dist/index.mjs +285 -0
- package/package.json +20 -20
- 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/README.md
CHANGED
|
@@ -1,58 +1,93 @@
|
|
|
1
|
-
# rio-license-checker
|
|
1
|
+
# [@rio-cloud/rio-license-checker](https://www.npmjs.com/package/@rio-cloud/rio-license-checker)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Helps checking third-party libraries' licenses according to RIO guidelines.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Usage
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* if a license is found that is not in the whitelist: fail with nonzero exit code
|
|
11
|
-
* if all licenses are compliant: upload a license report to the central RIO license S3 bucket (only if the `--upload` flag is set)
|
|
7
|
+
```shell
|
|
8
|
+
# general usage information
|
|
9
|
+
npx @rio-cloud/rio-license-checker help
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
# information on the default "run-check" command
|
|
12
|
+
npx @rio-cloud/rio-license-checker help run-check
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
# information on the "download-whitelist" command
|
|
15
|
+
npx @rio-cloud/rio-license-checker help download-whitelist
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
# information on the "upload-report" command
|
|
18
|
+
npx @rio-cloud/rio-license-checker help upload-report
|
|
19
|
+
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
The `run-check` command is the default command. The command name is optional:
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
```shell
|
|
24
|
+
npx @rio-cloud/rio-license-checker run-check -a rio-example -s example-service -t npm-frontend
|
|
25
|
+
# is the same as
|
|
26
|
+
npx @rio-cloud/rio-license-checker -a rio-example -s example-service -t npm-frontend
|
|
27
|
+
```
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
*
|
|
29
|
+
## Automatic mode (a.k.a. `run-check`)
|
|
30
|
+
|
|
31
|
+
1. Downloads the appropriate license whitelist that's curated by the RIO Security Guild from a central location.
|
|
32
|
+
2. Orchestrates a 3rd-party license checking tool to inspect your project's dependencies.
|
|
33
|
+
* If any dependency is found with non-compliant license information, this tool will exit with a non-zero code.
|
|
34
|
+
* If all dependencies are okay: generate a report file containing the list of dependencies, their versions, and their
|
|
35
|
+
respective license information.
|
|
36
|
+
3. Uploads the report to a central location (optional; this is only done when the `--upload` flag is set).
|
|
30
37
|
|
|
31
|
-
### gradle
|
|
38
|
+
### `gradle`
|
|
32
39
|
|
|
33
40
|
* The underlying license report tool is [hierynomus/license-gradle-plugin](https://github.com/hierynomus/license-gradle-plugin).
|
|
34
41
|
* You need to *include & configure the plugin in your `build.gradle.kts`*.
|
|
35
42
|
* The output of the plugin is compared with the whitelist programmatically (as we used to do in the `build.gradle.kts`).
|
|
36
43
|
* For subprojects, you need a separate invocation of the license checker, where the directory points to the subproject.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
* Currently, only subprojects directly below the root project are supported (limited by where the checker looks for
|
|
45
|
+
the gradle wrapper).
|
|
46
|
+
* When doing so, take care to specify a different service name to prevent overwriting the license report of the root
|
|
47
|
+
project.
|
|
48
|
+
* Please see below for examples.
|
|
40
49
|
|
|
41
|
-
```
|
|
50
|
+
```shell
|
|
42
51
|
npx @rio-cloud/rio-license-checker -a rio-example -s example-service -t gradle
|
|
43
52
|
npx @rio-cloud/rio-license-checker -a rio-example -s example-service_sub-project -t gradle -d ./sub-project
|
|
44
53
|
```
|
|
45
54
|
|
|
46
|
-
|
|
55
|
+
### `npm-frontend` and `npm-backend`
|
|
47
56
|
|
|
48
|
-
|
|
57
|
+
* The underlying license checker tool is [license-checker-rseidelsohn](https://www.npmjs.com/package/license-checker-rseidelsohn).
|
|
58
|
+
* The license checker generates a report and compares the licenses to the passed whitelist.
|
|
59
|
+
* The application's own package is automatically excluded from the report, as it (usually) does not have a license.
|
|
60
|
+
* For testability reasons, we cannot use the programmatic interface of the tool. Instead, we call it as a subprocess via
|
|
61
|
+
`zx`.
|
|
62
|
+
* You can exclude dependencies by creating a `oss-licenses-ignore-packages.txt` file in the project directory.
|
|
63
|
+
* Note that this will _statically_ inspect the dependencies as defined in your `package.json`. Especially for frontends,
|
|
64
|
+
this might not be accurate enough. Check out the "manual" type for `npm-frontend-bundled` below.
|
|
65
|
+
|
|
66
|
+
## `download-whitelist`
|
|
67
|
+
|
|
68
|
+
To just download the appropriate whitelist file as-is without any processing or running the check, you can use the
|
|
69
|
+
`download-whitelist` command:
|
|
49
70
|
|
|
50
71
|
```shell
|
|
51
|
-
npx @rio-cloud/rio-license-checker -
|
|
72
|
+
npx @rio-cloud/rio-license-checker download-whitelist -t npm-frontend-bundled
|
|
52
73
|
```
|
|
53
74
|
|
|
54
|
-
|
|
75
|
+
Note that this will only work when the current session has access to AWS.
|
|
76
|
+
|
|
77
|
+
Also note that this only supports the "npm-frontend-bundled" type at the moment, which will save the whitelist as
|
|
78
|
+
`frontend-license-whitelist.json` in the project directory.
|
|
79
|
+
|
|
80
|
+
This command supports a `directory` and `verbose` option, as well. Check out the usage docs for more info.
|
|
81
|
+
|
|
82
|
+
## `upload-report`
|
|
83
|
+
|
|
84
|
+
If your license check is handled by another tool during the build, you can use the `upload-report` command to upload the
|
|
85
|
+
report output from a given file.
|
|
55
86
|
|
|
56
87
|
```shell
|
|
57
|
-
npx @rio-cloud/rio-license-checker -a rio-example -s example-service -t npm-frontend
|
|
88
|
+
npx @rio-cloud/rio-license-checker upload-report -a rio-example -s example-service -t npm-frontend-bundled -r ./build/libraries.json
|
|
58
89
|
```
|
|
90
|
+
|
|
91
|
+
Note that this only supports the "npm-frontend-bundled" type at the moment.
|
|
92
|
+
|
|
93
|
+
This command supports a `verbose` option, as well. Check out the usage docs for more info.
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { Command, CommanderError, Option } from "commander";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
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 as fs$1, path as path$1 } from "zx";
|
|
10
|
+
|
|
11
|
+
//#region src/s3/license-bucket.ts
|
|
12
|
+
let ossLicensesBucketName;
|
|
13
|
+
const OSS_LICENSES_BUCKET_NAME_SSM_PARAMETER = "/config/oss-licenses/bucket-name";
|
|
14
|
+
const getOssLicensesBucketName = async () => {
|
|
15
|
+
if (!ossLicensesBucketName) {
|
|
16
|
+
const ssmClient = new SSMClient();
|
|
17
|
+
const command = new GetParameterCommand({ Name: OSS_LICENSES_BUCKET_NAME_SSM_PARAMETER });
|
|
18
|
+
const response = await ssmClient.send(command);
|
|
19
|
+
if (!response.Parameter?.Value) throw new Error("No license bucket name found");
|
|
20
|
+
ossLicensesBucketName = response.Parameter.Value;
|
|
21
|
+
logger.debug(`OSS Licenses Bucket: ${ossLicensesBucketName}`);
|
|
22
|
+
}
|
|
23
|
+
return ossLicensesBucketName;
|
|
24
|
+
};
|
|
25
|
+
const s3Keys = {
|
|
26
|
+
"npm-frontend": "whitelist-npm-frontend.txt",
|
|
27
|
+
"npm-backend": "whitelist-npm-backend.txt",
|
|
28
|
+
gradle: "whitelist-gradle.txt",
|
|
29
|
+
"npm-frontend-bundled": "whitelist-npm-frontend-bundled.json"
|
|
30
|
+
};
|
|
31
|
+
const downloadWhitelistFile = async (checkType) => {
|
|
32
|
+
const getWhitelistObject = new GetObjectCommand({
|
|
33
|
+
Bucket: await getOssLicensesBucketName(),
|
|
34
|
+
Key: s3Keys[checkType]
|
|
35
|
+
});
|
|
36
|
+
const whitelistResponse = await new S3Client().send(getWhitelistObject);
|
|
37
|
+
if (!whitelistResponse.Body) throw new Error("No whitelist found");
|
|
38
|
+
return whitelistResponse.Body.transformToString();
|
|
39
|
+
};
|
|
40
|
+
const downloadAutomaticWhitelist = async (checkType) => {
|
|
41
|
+
return (await downloadWhitelistFile(checkType)).split("\n").map(stripQuotes).filter(removeEmptyLines);
|
|
42
|
+
};
|
|
43
|
+
const stripQuotes = (line) => {
|
|
44
|
+
if (line.startsWith("\"") && line.endsWith("\"")) return line.slice(1, -1);
|
|
45
|
+
return line;
|
|
46
|
+
};
|
|
47
|
+
const removeEmptyLines = (line) => line.trim().length > 0;
|
|
48
|
+
const uploadLicenseReport = async (accountName, serviceName, type, report) => {
|
|
49
|
+
const s3Client = new S3Client();
|
|
50
|
+
const putObjectCommand = new PutObjectCommand({
|
|
51
|
+
Bucket: await getOssLicensesBucketName(),
|
|
52
|
+
Key: `reports/${accountName}/${serviceName}_${type}.txt`,
|
|
53
|
+
Body: report
|
|
54
|
+
});
|
|
55
|
+
await s3Client.send(putObjectCommand);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/cli/downloadWhitelistFileCommand.ts
|
|
60
|
+
const downloadWhitelistFileCommand = new Command("download-whitelist").description("Download the license whitelist (experimental)").addOption(new Option("-t, --type <type>", "Type of the license check. (only supports \"npm-frontend-bundled\" for now)").makeOptionMandatory(true).choices(["npm-frontend-bundled"])).option("-d, --directory <directory>", "Directory to save the downloaded whitelist file to", process.cwd()).option("-v, --verbose", "Enable debug logging", false).action(async ({ type, directory, verbose }) => {
|
|
61
|
+
logger.setLevel(verbose ? "debug" : "info");
|
|
62
|
+
const licenseWhitelistFileContent = await downloadWhitelistFile(type);
|
|
63
|
+
const prettyJson = JSON.stringify(JSON.parse(licenseWhitelistFileContent), null, 2);
|
|
64
|
+
logger.debug(`Licenses whitelist:\n----------------\n${prettyJson}\n----------------`);
|
|
65
|
+
const whitelistFilePath = await saveWhitelistFile(directory, type, licenseWhitelistFileContent);
|
|
66
|
+
logger.info(`Successfully downloaded whitelist file into ${whitelistFilePath}`);
|
|
67
|
+
});
|
|
68
|
+
const whitelistFilenames = { "npm-frontend-bundled": "frontend-license-whitelist.json" };
|
|
69
|
+
const saveWhitelistFile = async (directory, checkType, content) => {
|
|
70
|
+
const filename = whitelistFilenames[checkType];
|
|
71
|
+
const filePath = path.join(directory, filename);
|
|
72
|
+
await fs.promises.writeFile(filePath, content, "utf8");
|
|
73
|
+
return filePath;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/types.ts
|
|
78
|
+
var LicenseCheckError = class extends Error {
|
|
79
|
+
constructor(message) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.name = "LicenseCheckError";
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
var CliArgsError = class extends Error {
|
|
85
|
+
constructor(message) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.name = "CliArgsError";
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const automaticWhitelists = [
|
|
91
|
+
"npm-backend",
|
|
92
|
+
"npm-frontend",
|
|
93
|
+
"gradle"
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/integrations/license-checker-gradle.ts
|
|
98
|
+
const normalizeLicenseReport = (report) => {
|
|
99
|
+
const sortedLicences = report.licences.map((license) => ({
|
|
100
|
+
...license,
|
|
101
|
+
dependencies: [...license.dependencies].sort((a, b) => a.localeCompare(b))
|
|
102
|
+
})).sort((a, b) => {
|
|
103
|
+
const nameCompare = a.name.localeCompare(b.name);
|
|
104
|
+
if (nameCompare !== 0) return nameCompare;
|
|
105
|
+
const urlCompare = (a.url ?? "").localeCompare(b.url ?? "");
|
|
106
|
+
if (urlCompare !== 0) return urlCompare;
|
|
107
|
+
const dependenciesA = a.dependencies.join("|");
|
|
108
|
+
const dependenciesB = b.dependencies.join("|");
|
|
109
|
+
return dependenciesA.localeCompare(dependenciesB);
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
...report,
|
|
113
|
+
licences: sortedLicences
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Calls the gradle license checker with the given options. If a license is found that is not in the whitelist, an error is thrown.
|
|
118
|
+
* @param directory Folder containing the package.json to analyze
|
|
119
|
+
* @param licenseWhitelist List of allowed licenses
|
|
120
|
+
* @param excludePackages List of packages to exclude from the report, e.g. the current package
|
|
121
|
+
*
|
|
122
|
+
* @returns A dependency report generated by the license checker
|
|
123
|
+
*/
|
|
124
|
+
const callGradleLicenseChecker = async ({ directory, licenseWhitelist }) => {
|
|
125
|
+
const gradleWrapper = await discoverGradleWrapper(directory);
|
|
126
|
+
const result = await $({
|
|
127
|
+
cwd: directory,
|
|
128
|
+
nothrow: true,
|
|
129
|
+
quiet: true
|
|
130
|
+
})`./${gradleWrapper} downloadLicenses`;
|
|
131
|
+
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()}`);
|
|
132
|
+
const licensesByLicenseNameFile = path$1.join(directory, "build", "reports", "license", "license-dependency.json");
|
|
133
|
+
const licensesByLicenseName = normalizeLicenseReport(JSON.parse(await fs$1.readFile(licensesByLicenseNameFile, "utf8")));
|
|
134
|
+
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.");
|
|
135
|
+
const allowedLicenses = new Set(licenseWhitelist);
|
|
136
|
+
const notAllowedLicenses = /* @__PURE__ */ new Set();
|
|
137
|
+
for (const license of licensesByLicenseName.licences) if (!allowedLicenses.has(license.name)) {
|
|
138
|
+
notAllowedLicenses.add(license.name);
|
|
139
|
+
logger.warn(`License "${license.name}" is not in the whitelist. It is used by:\n${license.dependencies.join("\n")}`);
|
|
140
|
+
}
|
|
141
|
+
if (notAllowedLicenses.size > 0) throw new LicenseCheckError(`The following licenses are not in the whitelist: ${Array.from(notAllowedLicenses).sort().join(", ")}`);
|
|
142
|
+
return JSON.stringify(licensesByLicenseName, null, 2);
|
|
143
|
+
};
|
|
144
|
+
const discoverGradleWrapper = async (directory) => {
|
|
145
|
+
const gradleWrapperFile = path$1.join(directory, "gradlew");
|
|
146
|
+
if (fs$1.existsSync(gradleWrapperFile)) return "gradlew";
|
|
147
|
+
if (fs$1.existsSync(path$1.join(directory, "..", "gradlew"))) return "../gradlew";
|
|
148
|
+
throw new LicenseCheckError(`Could not find gradle wrapper in ${directory} or the parent directory. Please ensure that the gradle wrapper is present.`);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/integrations/license-checker-npm.ts
|
|
153
|
+
const resolveLicenseCheckerPath = () => {
|
|
154
|
+
return createRequire(import.meta.url).resolve("license-checker-rseidelsohn/bin/license-checker-rseidelsohn");
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Calls the npm license checker with the given options. If a license is found that is not in the whitelist, an error is thrown.
|
|
158
|
+
* @param directory Folder containing the package.json to analyze
|
|
159
|
+
* @param licenseWhitelist List of allowed licenses
|
|
160
|
+
* @param excludePackages List of packages to exclude from the report, e.g. the current package
|
|
161
|
+
*
|
|
162
|
+
* @returns A dependency report generated by the license checker
|
|
163
|
+
*/
|
|
164
|
+
const callNpmLicenseChecker = async ({ directory, licenseWhitelist, excludePackages }) => {
|
|
165
|
+
const licenseCheckExecutable = resolveLicenseCheckerPath();
|
|
166
|
+
if (licenseWhitelist.length === 0) throw new Error("No licenses in whitelist. This would allow all licenses, which is certainly not intended.");
|
|
167
|
+
const onlyAllowOption = licenseWhitelist.join(";");
|
|
168
|
+
const excludePackagesOption = excludePackages.join(";");
|
|
169
|
+
const result = await $({
|
|
170
|
+
cwd: directory,
|
|
171
|
+
nothrow: true,
|
|
172
|
+
quiet: true
|
|
173
|
+
})`${licenseCheckExecutable} --json --production --onlyAllow ${onlyAllowOption} --excludePackages ${excludePackagesOption} --relativeModulePath`;
|
|
174
|
+
if (result.exitCode !== 0) throw new LicenseCheckError(result.stderr.trimEnd());
|
|
175
|
+
return result.stdout;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/cli/validations.ts
|
|
180
|
+
const validateAccountName = (accountName) => {
|
|
181
|
+
if (!accountName.match(/^[a-zA-Z0-9-_]+$/)) throw new CliArgsError("Account name must only contain alphanumeric characters, hyphens and underscores");
|
|
182
|
+
};
|
|
183
|
+
const validateServiceName = (serviceName) => {
|
|
184
|
+
if (!serviceName.match(/^[a-zA-Z0-9-_]+$/)) throw new CliArgsError("Service name must only contain alphanumeric characters, hyphens and underscores");
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/cli/runCheckCommand.ts
|
|
189
|
+
const runCheckCommand = new Command("run-check").summary("(default) Run an automated license check and optionally upload the report to S3").description("This is the default command to be used for backend and \"traditional\" frontend projects.\n\nWill download the whitelist, then perform the check and upload the report in one single step.").addOption(new Option("-t, --type <type>", "Type of the license check").makeOptionMandatory(true).choices(automaticWhitelists)).option("-a, --account-name <account>", "Account / context name").option("-s, --service-name <service>", "Service name").option("-d, --directory <directory>", "Directory of the project to check licenses for", process.cwd()).option("-u, --upload", "Upload license report to S3", false).option("-v, --verbose", "Enable debug logging", false).action(async (options) => {
|
|
190
|
+
logger.setLevel(options.verbose ? "debug" : "info");
|
|
191
|
+
const contextName = options.accountName;
|
|
192
|
+
const serviceName = options.serviceName;
|
|
193
|
+
const checkType = options.type;
|
|
194
|
+
const upload = options.upload;
|
|
195
|
+
if (upload) {
|
|
196
|
+
if (!contextName) throw new CliArgsError("Account name must not be empty when --upload is specified");
|
|
197
|
+
validateAccountName(contextName);
|
|
198
|
+
if (!serviceName) throw new CliArgsError("Service name must not be empty when --upload is specified");
|
|
199
|
+
validateServiceName(serviceName);
|
|
200
|
+
}
|
|
201
|
+
const licenseWhitelist = await downloadAutomaticWhitelist(checkType);
|
|
202
|
+
logger.debug(`Licenses whitelist:\n----------------\n${licenseWhitelist.join("\n")}\n----------------`);
|
|
203
|
+
let result;
|
|
204
|
+
switch (checkType) {
|
|
205
|
+
case "npm-backend":
|
|
206
|
+
case "npm-frontend": {
|
|
207
|
+
if (!fs.existsSync(path.join(options.directory, "node_modules"))) throw new Error("No node_modules directory found. Please run \"npm ci\" before using the license checker.");
|
|
208
|
+
const excludeForApplicationPackage = getExcludeForApplicationPackage(options.directory);
|
|
209
|
+
const ignorePackages = readIgnorePackages(options.directory);
|
|
210
|
+
result = await callNpmLicenseChecker({
|
|
211
|
+
directory: options.directory,
|
|
212
|
+
licenseWhitelist,
|
|
213
|
+
excludePackages: [excludeForApplicationPackage, ...ignorePackages]
|
|
214
|
+
});
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case "gradle":
|
|
218
|
+
result = await callGradleLicenseChecker({
|
|
219
|
+
directory: options.directory,
|
|
220
|
+
licenseWhitelist
|
|
221
|
+
});
|
|
222
|
+
break;
|
|
223
|
+
default: throw new Error(`Unknown license check type: ${options.type}`);
|
|
224
|
+
}
|
|
225
|
+
logger.debug(`License report:\n${result.trimEnd()}`);
|
|
226
|
+
if (upload) {
|
|
227
|
+
await uploadLicenseReport(contextName, serviceName, checkType, result);
|
|
228
|
+
logger.info(`Uploaded license report for ${contextName}/${serviceName}_${checkType}`);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
const getExcludeForApplicationPackage = (directory) => {
|
|
232
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(directory, "package.json"), "utf8"));
|
|
233
|
+
const serviceVersion = packageJson.version;
|
|
234
|
+
return `${packageJson.name}@${serviceVersion}`;
|
|
235
|
+
};
|
|
236
|
+
const readIgnorePackages = (directory) => {
|
|
237
|
+
const ignorePackagesFile = path.join(directory, "oss-licenses-ignore-packages.txt");
|
|
238
|
+
let ignorePackages = [];
|
|
239
|
+
if (fs.existsSync(ignorePackagesFile)) ignorePackages = fs.readFileSync(ignorePackagesFile, "utf8").split("\n").filter((line) => line.trim() !== "");
|
|
240
|
+
return ignorePackages;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region src/cli/uploadReportCommand.ts
|
|
245
|
+
const uploadReportCommand = new Command("upload-report").description("Upload a license report (placeholder)").addOption(new Option("-t, --type <type>", "Type of the license check. (only supports \"npm-frontend-bundled\" for now)").makeOptionMandatory(true).choices(["npm-frontend-bundled"])).addOption(new Option("-a, --account-name <account>", "Account / context name").makeOptionMandatory(true)).addOption(new Option("-s, --service-name <service>", "Service name").makeOptionMandatory(true)).addOption(new Option("-r, --report-file <file>", "Path to the license report file").makeOptionMandatory(true)).option("-v, --verbose", "Enable debug logging", false).action(async ({ type, accountName, serviceName, reportFile, verbose }) => {
|
|
246
|
+
logger.setLevel(verbose ? "debug" : "info");
|
|
247
|
+
validateAccountName(accountName);
|
|
248
|
+
validateServiceName(serviceName);
|
|
249
|
+
const reportFileContent = await fs.promises.readFile(reportFile, "utf8");
|
|
250
|
+
logger.debug(`Report file:\n${reportFileContent}\n`);
|
|
251
|
+
await uploadLicenseReport(accountName, serviceName, type, reportFileContent);
|
|
252
|
+
logger.info(`Uploaded license report for ${accountName}/${serviceName}_${type}`);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/cli.ts
|
|
257
|
+
const wrapCommand = (command) => command.exitOverride((error) => {
|
|
258
|
+
throw error;
|
|
259
|
+
});
|
|
260
|
+
const cli = (args) => {
|
|
261
|
+
const program = new Command();
|
|
262
|
+
program.name("npx @rio-cloud/rio-license-checker").addCommand(wrapCommand(runCheckCommand), { isDefault: true }).addCommand(wrapCommand(downloadWhitelistFileCommand)).addCommand(wrapCommand(uploadReportCommand)).addHelpText("after", `\n"run-check" is the default command if you don't specify one.\nRun "npx @rio-cloud/rio-license-checker help run-check" for details.`);
|
|
263
|
+
return program.parseAsync(args);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/index.ts
|
|
268
|
+
try {
|
|
269
|
+
await cli(process.argv);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (error instanceof LicenseCheckError) {
|
|
272
|
+
console.error(error.message);
|
|
273
|
+
process.exit(2);
|
|
274
|
+
} else if (error instanceof CliArgsError) {
|
|
275
|
+
console.error(error.message);
|
|
276
|
+
process.exit(3);
|
|
277
|
+
} else if (error instanceof CommanderError) process.exit(error.exitCode);
|
|
278
|
+
else {
|
|
279
|
+
console.error("unknown error", error);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
//#endregion
|
|
285
|
+
export { };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rio-cloud/rio-license-checker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0-alpha.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
"organization": true
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
|
+
"build": "tsdown",
|
|
17
|
+
"dev": "tsdown --watch ./src",
|
|
16
18
|
"pretest": "tsc",
|
|
17
|
-
"test": "vitest
|
|
19
|
+
"test": "vitest run",
|
|
18
20
|
"vulnerability-check": "npm audit --registry https://registry.npmjs.org --parseable --audit-level=moderate --omit=dev",
|
|
19
21
|
"bump": "commit-and-tag-version -a --no-verify",
|
|
20
22
|
"release": "npm run test && npm run bump",
|
|
@@ -23,39 +25,37 @@
|
|
|
23
25
|
"release:push": "echo '✅ pushing release' && git push origin main --follow-tags"
|
|
24
26
|
},
|
|
25
27
|
"bin": {
|
|
26
|
-
"rio-license-checker": "
|
|
28
|
+
"rio-license-checker": "dist/index.mjs"
|
|
27
29
|
},
|
|
28
30
|
"files": [
|
|
29
|
-
"
|
|
30
|
-
"tsconfig.json",
|
|
31
|
-
"README.md",
|
|
32
|
-
"CHANGELOG.md"
|
|
31
|
+
"dist"
|
|
33
32
|
],
|
|
34
33
|
"dependencies": {
|
|
35
|
-
"@aws-sdk/client-s3": "3.
|
|
36
|
-
"@aws-sdk/client-ssm": "3.
|
|
37
|
-
"commander": "14.0.
|
|
34
|
+
"@aws-sdk/client-s3": "3.937.0",
|
|
35
|
+
"@aws-sdk/client-ssm": "3.936.0",
|
|
36
|
+
"commander": "14.0.2",
|
|
38
37
|
"license-checker-rseidelsohn": "4.4.2",
|
|
39
38
|
"loglevel": "1.9.2",
|
|
40
|
-
"
|
|
41
|
-
"zx": "8.8.1"
|
|
39
|
+
"zx": "8.8.5"
|
|
42
40
|
},
|
|
43
41
|
"devDependencies": {
|
|
44
|
-
"@biomejs/biome": "2.
|
|
42
|
+
"@biomejs/biome": "2.3.7",
|
|
45
43
|
"@rio-cloud/biome-config-claid": "1.0.0",
|
|
46
|
-
"@tsconfig/node22": "22.0.
|
|
44
|
+
"@tsconfig/node22": "22.0.5",
|
|
47
45
|
"@types/jest": "30.0.0",
|
|
48
46
|
"@types/license-checker": "25.0.6",
|
|
49
|
-
"@types/node": "24.
|
|
47
|
+
"@types/node": "24.10.1",
|
|
50
48
|
"aws-sdk-client-mock": "4.1.0",
|
|
51
49
|
"aws-sdk-client-mock-jest": "4.1.0",
|
|
52
|
-
"chalk": "5.6.
|
|
53
|
-
"commit-and-tag-version": "12.
|
|
54
|
-
"
|
|
55
|
-
"
|
|
50
|
+
"chalk": "5.6.2",
|
|
51
|
+
"commit-and-tag-version": "12.6.0",
|
|
52
|
+
"tsdown": "0.16.6",
|
|
53
|
+
"typescript": "5.9.3",
|
|
54
|
+
"vitest": "4.0.14"
|
|
56
55
|
},
|
|
57
56
|
"overrides": {
|
|
58
|
-
"form-data": ">=4.0.4"
|
|
57
|
+
"form-data": ">=4.0.4",
|
|
58
|
+
"glob": ">=10.5.0"
|
|
59
59
|
},
|
|
60
60
|
"commit-and-tag-version": {
|
|
61
61
|
"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
|
-
}
|