Merge pull request #423 from theztefan/allow-list-dependencies
Exclude dependencies from license checks
This commit is contained in:
@@ -66,19 +66,20 @@ jobs:
|
||||
|
||||
Configure this action by either inlining these options in your workflow file, or by using an external configuration file. All configuration options are optional.
|
||||
|
||||
| Option | Usage | Possible values | Default value |
|
||||
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------- |
|
||||
| `fail-on-severity` | Defines the threshold for the level of severity. The action will fail on any pull requests that introduce vulnerabilities of the specified severity level or higher. | `low`, `moderate`, `high`, `critical` | `low` |
|
||||
| `allow-licenses`* | Contains a list of allowed licenses. The action will fail on pull requests that introduce dependencies with licenses that do not match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
|
||||
| `deny-licenses`* | Contains a list of prohibited licenses. The action will fail on pull requests that introduce dependencies with licenses that match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
|
||||
| `fail-on-scopes`† | Contains a list of strings of the build environments you want to support. The action will fail on pull requests that introduce vulnerabilities in the scopes that match the list. | `runtime`, `development`, `unknown` | `runtime` |
|
||||
| `allow-ghsas` | Contains a list of GitHub Advisory Database IDs that can be skipped during detection. | Any GHSAs from the [GitHub Advisory Database](https://github.com/advisories) | none |
|
||||
| `license-check` | Enable or disable the license check performed by the action. | `true`, `false` | `true` |
|
||||
| `vulnerability-check` | Enable or disable the vulnerability check performed by the action. | `true`, `false` | `true` |
|
||||
| `base-ref`/`head-ref` | Provide custom git references for the git base/head when performing the comparison check. This is only used for event types other than `pull_request` and `pull_request_target`. | Any valid git ref(s) in your project | none |
|
||||
| `comment-summary-in-pr` | Enable or disable reporting the review summary as a comment in the pull request. If enabled, you must give the workflow or job permission `pull-requests: write`. | `true`, `false` | `false` |
|
||||
| Option | Usage | Possible values | Default value |
|
||||
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------- |
|
||||
| `fail-on-severity` | Defines the threshold for the level of severity. The action will fail on any pull requests that introduce vulnerabilities of the specified severity level or higher. | `low`, `moderate`, `high`, `critical` | `low` |
|
||||
| `allow-licenses`\* | Contains a list of allowed licenses. The action will fail on pull requests that introduce dependencies with licenses that do not match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
|
||||
| `deny-licenses`\* | Contains a list of prohibited licenses. The action will fail on pull requests that introduce dependencies with licenses that match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
|
||||
| `fail-on-scopes`† | Contains a list of strings of the build environments you want to support. The action will fail on pull requests that introduce vulnerabilities in the scopes that match the list. | `runtime`, `development`, `unknown` | `runtime` |
|
||||
| `allow-ghsas` | Contains a list of GitHub Advisory Database IDs that can be skipped during detection. | Any GHSAs from the [GitHub Advisory Database](https://github.com/advisories) | none |
|
||||
| `license-check` | Enable or disable the license check performed by the action. | `true`, `false` | `true` |
|
||||
| `vulnerability-check` | Enable or disable the vulnerability check performed by the action. | `true`, `false` | `true` |
|
||||
| `allow-dependencies-licenses`\* | Contains a list of packages that will be excluded from license checks. | Any package(s) in [purl](https://github.com/package-url/purl-spec) format | none |
|
||||
| `base-ref`/`head-ref` | Provide custom git references for the git base/head when performing the comparison check. This is only used for event types other than `pull_request` and `pull_request_target`. | Any valid git ref(s) in your project | none |
|
||||
| `comment-summary-in-pr` | Enable or disable reporting the review summary as a comment in the pull request. If enabled, you must give the workflow or job permission `pull-requests: write`. | `true`, `false` | `false` |
|
||||
|
||||
*not supported for use with GitHub Enterprise Server
|
||||
\*not supported for use with GitHub Enterprise Server
|
||||
|
||||
†will be supported with GitHub Enterprise Server 3.8
|
||||
|
||||
@@ -139,6 +140,8 @@ allow_licenses:
|
||||
- 'MIT'
|
||||
```
|
||||
|
||||
For more examples of how to use this action and its configuration options, see the [examples](docs/examples.md) page.
|
||||
|
||||
### Considerations
|
||||
|
||||
- Checking for licenses is not supported on Enterprise Server.
|
||||
|
||||
@@ -49,6 +49,32 @@ const rubyChange: Change = {
|
||||
]
|
||||
}
|
||||
|
||||
const pipChange: Change = {
|
||||
change_type: 'added',
|
||||
manifest: 'requirements.txt',
|
||||
ecosystem: 'pip',
|
||||
name: 'package-1',
|
||||
version: '1.1.1',
|
||||
package_url: 'pkg:pip/[email protected]',
|
||||
license: 'MIT',
|
||||
source_repository_url: 'github.com/some-repo',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'moderate',
|
||||
advisory_ghsa_id: 'second-random_string',
|
||||
advisory_summary: 'not so dangerous',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
},
|
||||
{
|
||||
severity: 'low',
|
||||
advisory_ghsa_id: 'third-random_string',
|
||||
advisory_summary: 'dont page me',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
jest.mock('@actions/core')
|
||||
|
||||
const mockOctokit = {
|
||||
@@ -153,6 +179,34 @@ test('it adds all licenses to unresolved if it is unable to determine the validi
|
||||
expect(invalidLicenses.unresolved.length).toEqual(2)
|
||||
})
|
||||
|
||||
test('it does not filter out changes that are on the exclusions list', async () => {
|
||||
const changes: Changes = [pipChange, npmChange, rubyChange]
|
||||
const licensesConfig = {
|
||||
allow: ['BSD'],
|
||||
licenseExclusions: ['pkg:pip/[email protected]', 'pkg:npm/[email protected]']
|
||||
}
|
||||
const invalidLicenses = await getInvalidLicenseChanges(
|
||||
changes,
|
||||
licensesConfig
|
||||
)
|
||||
expect(invalidLicenses.forbidden.length).toEqual(0)
|
||||
})
|
||||
|
||||
test('it does filters out changes if they are not on the exclusions list', async () => {
|
||||
const changes: Changes = [pipChange, npmChange, rubyChange]
|
||||
const licensesConfig = {
|
||||
allow: ['BSD'],
|
||||
licenseExclusions: ['pkg:pip/[email protected]', 'pkg:npm/[email protected]']
|
||||
}
|
||||
const invalidLicenses = await getInvalidLicenseChanges(
|
||||
changes,
|
||||
licensesConfig
|
||||
)
|
||||
expect(invalidLicenses.forbidden.length).toEqual(2)
|
||||
expect(invalidLicenses.forbidden[0]).toBe(pipChange)
|
||||
expect(invalidLicenses.forbidden[1]).toBe(npmChange)
|
||||
})
|
||||
|
||||
describe('GH License API fallback', () => {
|
||||
test('it calls licenses endpoint if atleast one of the changes has null license and valid source_repository_url', async () => {
|
||||
const nullLicenseChange = {
|
||||
|
||||
@@ -29,6 +29,9 @@ inputs:
|
||||
deny-licenses:
|
||||
description: Comma-separated list of forbidden licenses (e.g. "MIT, GPL 3.0, BSD 2 Clause")
|
||||
required: false
|
||||
allow-dependencies-licenses:
|
||||
description: Comma-separated list of dependencies in purl format (e.g. "pkg:npm/express, pkg:pip/pycrypto"). These dependencies will be permitted to use any license, no matter what license policy is enforced otherwise.
|
||||
required: false
|
||||
allow-ghsas:
|
||||
description: Comma-separated list of allowed GitHub Advisory IDs (e.g. "GHSA-abcd-1234-5679, GHSA-efgh-1234-5679")
|
||||
required: false
|
||||
|
||||
+293
-2
@@ -272,10 +272,30 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.getInvalidLicenseChanges = void 0;
|
||||
const spdx_satisfies_1 = __importDefault(__nccwpck_require__(4424));
|
||||
const utils_1 = __nccwpck_require__(918);
|
||||
const packageurl_js_1 = __nccwpck_require__(8915);
|
||||
function getInvalidLicenseChanges(changes, licenses) {
|
||||
var _a;
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const { allow, deny } = licenses;
|
||||
const licenseExclusions = (_a = licenses.licenseExclusions) === null || _a === void 0 ? void 0 : _a.map((pkgUrl) => {
|
||||
return packageurl_js_1.PackageURL.fromString(pkgUrl);
|
||||
});
|
||||
const groupedChanges = yield groupChanges(changes);
|
||||
// Takes the changes from the groupedChanges object and filters out the ones that are part of the exclusions list
|
||||
// It does by creating a new PackageURL object from the change and comparing it to the exclusions list
|
||||
groupedChanges.licensed = groupedChanges.licensed.filter(change => {
|
||||
const changeAsPackageURL = packageurl_js_1.PackageURL.fromString(change.package_url);
|
||||
// We want to find if the licenseExclussion list contains the PackageURL of the Change
|
||||
// If it does, we want to filter it out and therefore return false
|
||||
// If it doesn't, we want to keep it and therefore return true
|
||||
if (licenseExclusions !== null &&
|
||||
licenseExclusions !== undefined &&
|
||||
licenseExclusions.findIndex(exclusion => exclusion.type === changeAsPackageURL.type &&
|
||||
exclusion.name === changeAsPackageURL.name) !== -1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const licensedChanges = groupedChanges.licensed;
|
||||
const invalidLicenseChanges = {
|
||||
unlicensed: groupedChanges.unlicensed,
|
||||
@@ -485,7 +505,8 @@ function run() {
|
||||
change.vulnerabilities.length > 0);
|
||||
const invalidLicenseChanges = yield (0, licenses_1.getInvalidLicenseChanges)(filteredChanges, {
|
||||
allow: config.allow_licenses,
|
||||
deny: config.deny_licenses
|
||||
deny: config.deny_licenses,
|
||||
licenseExclusions: config.allow_dependencies_licenses
|
||||
});
|
||||
summary.addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, config);
|
||||
if (snapshot_warnings) {
|
||||
@@ -684,6 +705,7 @@ exports.ConfigurationOptionsSchema = z
|
||||
fail_on_scopes: z.array(z.enum(exports.SCOPES)).default(['runtime']),
|
||||
allow_licenses: z.array(z.string()).optional(),
|
||||
deny_licenses: z.array(z.string()).optional(),
|
||||
allow_dependencies_licenses: z.array(z.string()).optional(),
|
||||
allow_ghsas: z.array(z.string()).default([]),
|
||||
license_check: z.boolean().default(true),
|
||||
vulnerability_check: z.boolean().default(true),
|
||||
@@ -853,6 +875,9 @@ function addLicensesToSummary(invalidLicenseChanges, config) {
|
||||
if (config.deny_licenses && config.deny_licenses.length > 0) {
|
||||
core.summary.addQuote(`<strong>Denied Licenses</strong>: ${config.deny_licenses.join(', ')}`);
|
||||
}
|
||||
if (config.allow_dependencies_licenses) {
|
||||
core.summary.addQuote(`<strong>Excluded from license check</strong>: ${config.allow_dependencies_licenses.join(', ')}`);
|
||||
}
|
||||
core.debug(`found ${invalidLicenseChanges.unlicensed.length} unknown licenses`);
|
||||
core.debug(`${invalidLicenseChanges.unresolved.length} licenses could not be validated`);
|
||||
}
|
||||
@@ -37498,6 +37523,251 @@ function onceStrict (fn) {
|
||||
}
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 8915:
|
||||
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
|
||||
|
||||
/*!
|
||||
Copyright (c) the purl authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
const PackageURL = __nccwpck_require__(8749);
|
||||
|
||||
module.exports = {
|
||||
PackageURL
|
||||
};
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 8749:
|
||||
/***/ ((module) => {
|
||||
|
||||
/*!
|
||||
Copyright (c) the purl authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
const KnownQualifierNames = Object.freeze({
|
||||
// known qualifiers as defined here:
|
||||
// https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst#known-qualifiers-keyvalue-pairs
|
||||
RepositoryUrl: 'repository_url',
|
||||
DownloadUrl: 'download_url',
|
||||
VcsUrl: 'vcs_url',
|
||||
FileName: 'file_name',
|
||||
Checksum: 'checksum'
|
||||
});
|
||||
|
||||
class PackageURL {
|
||||
|
||||
static get KnownQualifierNames() {
|
||||
return KnownQualifierNames;
|
||||
}
|
||||
|
||||
constructor(type, namespace, name, version, qualifiers, subpath) {
|
||||
let required = { 'type': type, 'name': name };
|
||||
Object.keys(required).forEach(key => {
|
||||
if (!required[key]) {
|
||||
throw new Error('Invalid purl: "' + key + '" is a required field.');
|
||||
}
|
||||
});
|
||||
|
||||
let strings = { 'type': type, 'namespace': namespace, 'name': name, 'versions': version, 'subpath': subpath };
|
||||
Object.keys(strings).forEach(key => {
|
||||
if (strings[key] && typeof strings[key] === 'string' || !strings[key]) {
|
||||
return;
|
||||
}
|
||||
throw new Error('Invalid purl: "' + key + '" argument must be a string.');
|
||||
});
|
||||
|
||||
if (qualifiers) {
|
||||
if (typeof qualifiers !== 'object') {
|
||||
throw new Error('Invalid purl: "qualifiers" argument must be a dictionary.');
|
||||
}
|
||||
Object.keys(qualifiers).forEach(key => {
|
||||
if (!/^[a-z]+$/i.test(key) && !/[\.-_]/.test(key)) {
|
||||
throw new Error('Invalid purl: qualifier "' + key + '" contains an illegal character.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.type = type;
|
||||
this.name = name;
|
||||
this.namespace = namespace;
|
||||
this.version = version;
|
||||
this.qualifiers = qualifiers;
|
||||
this.subpath = subpath;
|
||||
}
|
||||
|
||||
_handlePyPi() {
|
||||
this.name = this.name.toLowerCase().replace(/_/g, '-');
|
||||
}
|
||||
|
||||
toString() {
|
||||
var purl = ['pkg:', encodeURIComponent(this.type), '/'];
|
||||
|
||||
if (this.type === 'pypi') {
|
||||
this._handlePyPi();
|
||||
}
|
||||
|
||||
if (this.namespace) {
|
||||
purl.push(
|
||||
encodeURIComponent(this.namespace)
|
||||
.replace(/%3A/g, ':')
|
||||
.replace(/%2F/g, '/')
|
||||
);
|
||||
purl.push('/');
|
||||
}
|
||||
|
||||
purl.push(encodeURIComponent(this.name).replace(/%3A/g, ':'));
|
||||
|
||||
if (this.version) {
|
||||
purl.push('@');
|
||||
purl.push(encodeURIComponent(this.version).replace(/%3A/g, ':'));
|
||||
}
|
||||
|
||||
if (this.qualifiers) {
|
||||
purl.push('?');
|
||||
|
||||
let qualifiers = this.qualifiers;
|
||||
let qualifierString = [];
|
||||
Object.keys(qualifiers).sort().forEach(key => {
|
||||
qualifierString.push(
|
||||
encodeURIComponent(key).replace(/%3A/g, ':')
|
||||
+ '='
|
||||
+ encodeURIComponent(qualifiers[key]).replace(/%2F/g, '/')
|
||||
);
|
||||
});
|
||||
|
||||
purl.push(qualifierString.join('&'));
|
||||
}
|
||||
|
||||
if (this.subpath) {
|
||||
purl.push('#');
|
||||
purl.push(encodeURIComponent(this.subpath)
|
||||
.replace(/%3A/g, ':')
|
||||
.replace(/%2F/g, '/'));
|
||||
}
|
||||
|
||||
return purl.join('');
|
||||
}
|
||||
|
||||
static fromString(purl) {
|
||||
if (!purl || typeof purl !== 'string' || !purl.trim()) {
|
||||
throw new Error('A purl string argument is required.');
|
||||
}
|
||||
|
||||
let [scheme, remainder] = purl.split(':', 2);
|
||||
if (scheme !== 'pkg') {
|
||||
throw new Error('purl is missing the required "pkg" scheme component.');
|
||||
}
|
||||
// this strip '/, // and /// as possible in :// or :///
|
||||
// from https://gist.github.com/refo/47632c8a547f2d9b6517#file-remove-leading-slash
|
||||
remainder = remainder.trim().replace(/^\/+/g, '');
|
||||
|
||||
let type
|
||||
[type, remainder] = remainder.split('/', 2);
|
||||
if (!type || !remainder) {
|
||||
throw new Error('purl is missing the required "type" component.');
|
||||
}
|
||||
type = decodeURIComponent(type)
|
||||
|
||||
let url = new URL(purl);
|
||||
|
||||
let qualifiers = null;
|
||||
url.searchParams.forEach((value, key) => {
|
||||
if (!qualifiers) {
|
||||
qualifiers = {};
|
||||
}
|
||||
qualifiers[key] = value;
|
||||
});
|
||||
let subpath = url.hash;
|
||||
if (subpath.indexOf('#') === 0) {
|
||||
subpath = subpath.substring(1);
|
||||
}
|
||||
subpath = subpath.length === 0
|
||||
? null
|
||||
: decodeURIComponent(subpath)
|
||||
|
||||
if (url.username !== '' || url.password !== '') {
|
||||
throw new Error('Invalid purl: cannot contain a "user:pass@host:port"');
|
||||
}
|
||||
|
||||
// this strip '/, // and /// as possible in :// or :///
|
||||
// from https://gist.github.com/refo/47632c8a547f2d9b6517#file-remove-leading-slash
|
||||
let path = url.pathname.trim().replace(/^\/+/g, '');
|
||||
|
||||
// version is optional - check for existence
|
||||
let version = null;
|
||||
if (path.includes('@')) {
|
||||
let index = path.indexOf('@');
|
||||
version = decodeURIComponent(path.substring(index + 1));
|
||||
remainder = path.substring(0, index);
|
||||
} else {
|
||||
remainder = path;
|
||||
}
|
||||
|
||||
// The 'remainder' should now consist of an optional namespace and the name
|
||||
let remaining = remainder.split('/').slice(1);
|
||||
let name = null;
|
||||
let namespace = null;
|
||||
if (remaining.length > 1) {
|
||||
let nameIndex = remaining.length - 1;
|
||||
let namespaceComponents = remaining.slice(0, nameIndex);
|
||||
name = decodeURIComponent(remaining[nameIndex]);
|
||||
namespace = decodeURIComponent(namespaceComponents.join('/'));
|
||||
} else if (remaining.length === 1) {
|
||||
name = decodeURIComponent(remaining[0]);
|
||||
}
|
||||
|
||||
if (name === '') {
|
||||
throw new Error('purl is missing the required "name" component.');
|
||||
}
|
||||
|
||||
return new PackageURL(type, namespace, name, version, qualifiers, subpath);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = PackageURL;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 1867:
|
||||
@@ -45900,6 +46170,7 @@ const core = __importStar(__nccwpck_require__(2186));
|
||||
const z = __importStar(__nccwpck_require__(3301));
|
||||
const schemas_1 = __nccwpck_require__(1129);
|
||||
const utils_1 = __nccwpck_require__(1314);
|
||||
const packageurl_js_1 = __nccwpck_require__(8915);
|
||||
function readConfig() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const inlineConfig = readInlineConfig();
|
||||
@@ -45917,12 +46188,14 @@ function readInlineConfig() {
|
||||
const fail_on_scopes = parseList(getOptionalInput('fail-on-scopes'));
|
||||
const allow_licenses = parseList(getOptionalInput('allow-licenses'));
|
||||
const deny_licenses = parseList(getOptionalInput('deny-licenses'));
|
||||
const allow_dependencies_licenses = parseList(getOptionalInput('allow-dependencies-licenses'));
|
||||
const allow_ghsas = parseList(getOptionalInput('allow-ghsas'));
|
||||
const license_check = getOptionalBoolean('license-check');
|
||||
const vulnerability_check = getOptionalBoolean('vulnerability-check');
|
||||
const base_ref = getOptionalInput('base-ref');
|
||||
const head_ref = getOptionalInput('head-ref');
|
||||
const comment_summary_in_pr = getOptionalBoolean('comment-summary-in-pr');
|
||||
validatePURL(allow_dependencies_licenses);
|
||||
validateLicenses('allow-licenses', allow_licenses);
|
||||
validateLicenses('deny-licenses', deny_licenses);
|
||||
const keys = {
|
||||
@@ -45930,6 +46203,7 @@ function readInlineConfig() {
|
||||
fail_on_scopes,
|
||||
allow_licenses,
|
||||
deny_licenses,
|
||||
allow_dependencies_licenses,
|
||||
allow_ghsas,
|
||||
license_check,
|
||||
vulnerability_check,
|
||||
@@ -45998,7 +46272,8 @@ function parseConfigFile(configData) {
|
||||
'allow-licenses',
|
||||
'deny-licenses',
|
||||
'fail-on-scopes',
|
||||
'allow-ghsas'
|
||||
'allow-ghsas',
|
||||
'allow-dependencies-licenses'
|
||||
];
|
||||
for (const key of Object.keys(data)) {
|
||||
// strings can contain list values (e.g. 'MIT, Apache-2.0'). In this
|
||||
@@ -46013,6 +46288,10 @@ function parseConfigFile(configData) {
|
||||
if (key === 'allow-licenses' || key === 'deny-licenses') {
|
||||
validateLicenses(key, data[key]);
|
||||
}
|
||||
// validate purls from the allow-dependencies-licenses
|
||||
if (key === 'allow-dependencies-licenses') {
|
||||
validatePURL(data[key]);
|
||||
}
|
||||
// get rid of the ugly dashes from the actions conventions
|
||||
if (key.includes('-')) {
|
||||
data[key.replace(/-/g, '_')] = data[key];
|
||||
@@ -46048,6 +46327,17 @@ function getRemoteConfig(configOpts) {
|
||||
}
|
||||
});
|
||||
}
|
||||
function validatePURL(allow_dependencies_licenses) {
|
||||
//validate that the provided elements of the string are in valid purl format
|
||||
if (allow_dependencies_licenses === undefined) {
|
||||
return;
|
||||
}
|
||||
const invalid_purls = allow_dependencies_licenses.filter(purl => !packageurl_js_1.PackageURL.fromString(purl));
|
||||
if (invalid_purls.length > 0) {
|
||||
throw new Error(`Invalid purl(s) in allow-dependencies-licenses: ${invalid_purls}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/***/ }),
|
||||
@@ -46195,6 +46485,7 @@ exports.ConfigurationOptionsSchema = z
|
||||
fail_on_scopes: z.array(z.enum(exports.SCOPES)).default(['runtime']),
|
||||
allow_licenses: z.array(z.string()).optional(),
|
||||
deny_licenses: z.array(z.string()).optional(),
|
||||
allow_dependencies_licenses: z.array(z.string()).optional(),
|
||||
allow_ghsas: z.array(z.string()).default([]),
|
||||
license_check: z.boolean().default(true),
|
||||
vulnerability_check: z.boolean().default(true),
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+22
@@ -1340,6 +1340,28 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
packageurl-js
|
||||
MIT
|
||||
Copyright (c) the purl authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
safe-buffer
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
# Examples on how to use the Dependancy Review Action
|
||||
|
||||
## Basic Usage
|
||||
|
||||
A very basic example of how to use the action. This will run the action with the default configuration.
|
||||
|
||||
The full list of configuration options can be found [here](../README.md#configuration-options).
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
```
|
||||
|
||||
## Using an inline configuration
|
||||
|
||||
The following example will fail the action if any vulnerabilities are found with a severity of medium or higher; and if any packages are found with an incompatible license - in this case, the LGPL-2.0 and BSD-2-Clause licenses.
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
with:
|
||||
fail-on-severity: critical
|
||||
deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||
```
|
||||
|
||||
## Using a configuration file
|
||||
|
||||
The following example will use a configuration file to configure the action. This is useful if you want to keep your configuration in a single place and makes it easier to manage as the configuration grows.
|
||||
|
||||
The configuration file can be located in the same repository or in a separate repository. Having it in a separate repository might be useful if you plan to use the same configuration across multiple repositories and control it centrally.
|
||||
|
||||
In this example, the configuration file is located in the same repository under `.github/dependency-review-config.yml`. The following configuration will fail the action if any vulnerabilities are found with a severity of critical; and if any packages are found with an incompatible license - in this case, the LGPL-2.0 and BSD-2-Clause licenses.
|
||||
|
||||
```yaml
|
||||
fail_on_severity: 'critical'
|
||||
allow_licenses:
|
||||
- 'LGPL-2.0'
|
||||
- 'BSD-2-Clause'
|
||||
```
|
||||
|
||||
The Dependancy Review Action workflow file will then look like this:
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
with:
|
||||
config-file: './.github/dependency-review-config.yml'
|
||||
```
|
||||
|
||||
## Using a configuration file from a external repository
|
||||
|
||||
The following example will use a configuration file from an external public GitHub repository to configure the action.
|
||||
|
||||
Let's say that the configuration file is located in `github/octorepo/dependency-review-config.yml@main`
|
||||
|
||||
The Dependancy Review Action workflow file will then look like this:
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
with:
|
||||
config-file: 'github/octorepo/dependency-review-config.yml@main'
|
||||
```
|
||||
|
||||
## Using a configuration file from a external repository with a personal access token
|
||||
|
||||
The following example will use a configuration file from an external private GtiHub repository to configure the action.
|
||||
|
||||
Let's say that the configuration file is located in `github/octorepo-private/dependency-review-config.yml@main`
|
||||
|
||||
The Dependancy Review Action workflow file will then look like this:
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
with:
|
||||
config-file: 'github/octorepo-private/dependency-review-config.yml@main'
|
||||
config-file-token: ${{ secrets.GITHUB_TOKEN }} # or a personal access token
|
||||
```
|
||||
|
||||
## Getting the results of the action in the PR as a comment
|
||||
|
||||
Using the `comment-summary-in-pr` you can get the results of the action in the PR as a comment. In order for this to work, the action needs to be able to create a comment in the PR. This requires additional `pull-requests: write` permission.
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
with:
|
||||
fail-on-severity: critical
|
||||
deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||
comment-summary-in-pr: true
|
||||
```
|
||||
|
||||
## Exclude dependencies from the license check
|
||||
|
||||
Using the `allow-dependencies-licenses` you can exclude dependencies from the license check. The values should be provided in [purl](https://github.com/package-url/purl-spec) format.
|
||||
|
||||
In this example, we are excluding `lodash` from `npm` and `requests` from `pip` dependencies from the license check
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
with:
|
||||
fail-on-severity: critical
|
||||
deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||
comment-summary-in-pr: true
|
||||
allow-dependencies-licenses: 'pkg:npm/loadash, pkg:pip/requests'
|
||||
```
|
||||
|
||||
If we were to use configuration file, the configuration would look like this:
|
||||
|
||||
```yaml
|
||||
fail-on-severity: 'critical'
|
||||
allow-licenses:
|
||||
- 'LGPL-2.0'
|
||||
- 'BSD-2-Clause'
|
||||
allow-dependencies-licenses:
|
||||
- 'pkg:npm/loadash'
|
||||
- 'pkg:pip/requests'
|
||||
```
|
||||
|
||||
## Only check for vulnerabilities
|
||||
|
||||
To only do the vulnerability check you can use the `license-check` to disable the license compatibility check (which is done by default).
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
with:
|
||||
fail-on-severity: critical
|
||||
comment-summary-in-pr: true
|
||||
license-check: false
|
||||
```
|
||||
Generated
+9
-6839
File diff suppressed because it is too large
Load Diff
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dependency-review-action",
|
||||
"version": "3.0.4",
|
||||
"version": "3.0.5",
|
||||
"private": true,
|
||||
"description": "A GitHub Action for Dependency Review",
|
||||
"main": "lib/main.js",
|
||||
@@ -33,6 +33,7 @@
|
||||
"got": "^12.6.0",
|
||||
"nodemon": "^2.0.22",
|
||||
"octokit": "^2.0.16",
|
||||
"packageurl-js": "^1.0.2",
|
||||
"spdx-expression-parse": "^3.0.1",
|
||||
"spdx-satisfies": "^5.0.1",
|
||||
"yaml": "^2.3.1",
|
||||
@@ -59,4 +60,4 @@
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,12 @@ const defaultConfig: ConfigurationOptions = {
|
||||
allow_ghsas: [],
|
||||
allow_licenses: ['MIT'],
|
||||
deny_licenses: [],
|
||||
allow_dependencies_licenses: [
|
||||
'pkg:npm/[email protected]',
|
||||
'pkg:pip/requests',
|
||||
'pkg:pip/certifi',
|
||||
'pkg:pip/[email protected]'
|
||||
],
|
||||
comment_summary_in_pr: true
|
||||
}
|
||||
|
||||
|
||||
+29
-1
@@ -5,6 +5,7 @@ import * as core from '@actions/core'
|
||||
import * as z from 'zod'
|
||||
import {ConfigurationOptions, ConfigurationOptionsSchema} from './schemas'
|
||||
import {isSPDXValid, octokitClient} from './utils'
|
||||
import {PackageURL} from 'packageurl-js'
|
||||
|
||||
type ConfigurationOptionsPartial = Partial<ConfigurationOptions>
|
||||
|
||||
@@ -29,6 +30,9 @@ function readInlineConfig(): ConfigurationOptionsPartial {
|
||||
const fail_on_scopes = parseList(getOptionalInput('fail-on-scopes'))
|
||||
const allow_licenses = parseList(getOptionalInput('allow-licenses'))
|
||||
const deny_licenses = parseList(getOptionalInput('deny-licenses'))
|
||||
const allow_dependencies_licenses = parseList(
|
||||
getOptionalInput('allow-dependencies-licenses')
|
||||
)
|
||||
const allow_ghsas = parseList(getOptionalInput('allow-ghsas'))
|
||||
const license_check = getOptionalBoolean('license-check')
|
||||
const vulnerability_check = getOptionalBoolean('vulnerability-check')
|
||||
@@ -36,6 +40,7 @@ function readInlineConfig(): ConfigurationOptionsPartial {
|
||||
const head_ref = getOptionalInput('head-ref')
|
||||
const comment_summary_in_pr = getOptionalBoolean('comment-summary-in-pr')
|
||||
|
||||
validatePURL(allow_dependencies_licenses)
|
||||
validateLicenses('allow-licenses', allow_licenses)
|
||||
validateLicenses('deny-licenses', deny_licenses)
|
||||
|
||||
@@ -44,6 +49,7 @@ function readInlineConfig(): ConfigurationOptionsPartial {
|
||||
fail_on_scopes,
|
||||
allow_licenses,
|
||||
deny_licenses,
|
||||
allow_dependencies_licenses,
|
||||
allow_ghsas,
|
||||
license_check,
|
||||
vulnerability_check,
|
||||
@@ -130,7 +136,8 @@ function parseConfigFile(configData: string): ConfigurationOptionsPartial {
|
||||
'allow-licenses',
|
||||
'deny-licenses',
|
||||
'fail-on-scopes',
|
||||
'allow-ghsas'
|
||||
'allow-ghsas',
|
||||
'allow-dependencies-licenses'
|
||||
]
|
||||
|
||||
for (const key of Object.keys(data)) {
|
||||
@@ -149,6 +156,11 @@ function parseConfigFile(configData: string): ConfigurationOptionsPartial {
|
||||
validateLicenses(key, data[key])
|
||||
}
|
||||
|
||||
// validate purls from the allow-dependencies-licenses
|
||||
if (key === 'allow-dependencies-licenses') {
|
||||
validatePURL(data[key])
|
||||
}
|
||||
|
||||
// get rid of the ugly dashes from the actions conventions
|
||||
if (key.includes('-')) {
|
||||
data[key.replace(/-/g, '_')] = data[key]
|
||||
@@ -187,3 +199,19 @@ async function getRemoteConfig(configOpts: {
|
||||
throw new Error('Error fetching remote config file')
|
||||
}
|
||||
}
|
||||
function validatePURL(allow_dependencies_licenses: string[] | undefined): void {
|
||||
//validate that the provided elements of the string are in valid purl format
|
||||
if (allow_dependencies_licenses === undefined) {
|
||||
return
|
||||
}
|
||||
const invalid_purls = allow_dependencies_licenses.filter(
|
||||
purl => !PackageURL.fromString(purl)
|
||||
)
|
||||
|
||||
if (invalid_purls.length > 0) {
|
||||
throw new Error(
|
||||
`Invalid purl(s) in allow-dependencies-licenses: ${invalid_purls}`
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
+31
-1
@@ -1,17 +1,19 @@
|
||||
import spdxSatisfies from 'spdx-satisfies'
|
||||
import {Change, Changes} from './schemas'
|
||||
import {isSPDXValid, octokitClient} from './utils'
|
||||
import {PackageURL} from 'packageurl-js'
|
||||
|
||||
/**
|
||||
* Loops through a list of changes, filtering and returning the
|
||||
* ones that don't conform to the licenses allow/deny lists.
|
||||
* It will also filter out the changes which are defined in the licenseExclusions list.
|
||||
*
|
||||
* Keep in mind that we don't let users specify both an allow and a deny
|
||||
* list in their config files, so this code works under the assumption that
|
||||
* one of the two list parameters will be empty. If both lists are provided,
|
||||
* we will ignore the deny list.
|
||||
* @param {Change[]} changes The list of changes to filter.
|
||||
* @param { { allow?: string[], deny?: string[]}} licenses An object with `allow`/`deny` keys, each containing a list of licenses.
|
||||
* @param { { allow?: string[], deny?: string[], licenseExclusions?: string[]}} licenses An object with `allow`/`deny`/`licenseExclusions` keys, each containing a list of licenses.
|
||||
* @returns {Promise<{Object.<string, Array.<Change>>}} A promise to a Record Object. The keys are strings, unlicensed, unresolved and forbidden. The values are a list of changes
|
||||
*/
|
||||
export type InvalidLicenseChangeTypes =
|
||||
@@ -24,11 +26,39 @@ export async function getInvalidLicenseChanges(
|
||||
licenses: {
|
||||
allow?: string[]
|
||||
deny?: string[]
|
||||
licenseExclusions?: string[]
|
||||
}
|
||||
): Promise<InvalidLicenseChanges> {
|
||||
const {allow, deny} = licenses
|
||||
const licenseExclusions = licenses.licenseExclusions?.map(
|
||||
(pkgUrl: string) => {
|
||||
return PackageURL.fromString(pkgUrl)
|
||||
}
|
||||
)
|
||||
|
||||
const groupedChanges = await groupChanges(changes)
|
||||
|
||||
// Takes the changes from the groupedChanges object and filters out the ones that are part of the exclusions list
|
||||
// It does by creating a new PackageURL object from the change and comparing it to the exclusions list
|
||||
groupedChanges.licensed = groupedChanges.licensed.filter(change => {
|
||||
const changeAsPackageURL = PackageURL.fromString(change.package_url)
|
||||
|
||||
// We want to find if the licenseExclussion list contains the PackageURL of the Change
|
||||
// If it does, we want to filter it out and therefore return false
|
||||
// If it doesn't, we want to keep it and therefore return true
|
||||
if (
|
||||
licenseExclusions !== null &&
|
||||
licenseExclusions !== undefined &&
|
||||
licenseExclusions.findIndex(
|
||||
exclusion =>
|
||||
exclusion.type === changeAsPackageURL.type &&
|
||||
exclusion.name === changeAsPackageURL.name
|
||||
) !== -1
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
const licensedChanges: Changes = groupedChanges.licensed
|
||||
|
||||
const invalidLicenseChanges: InvalidLicenseChanges = {
|
||||
|
||||
+3
-1
@@ -20,6 +20,7 @@ import {commentPr} from './comment-pr'
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
const config = await readConfig()
|
||||
|
||||
const refs = getRefs(config, github.context)
|
||||
|
||||
const comparison = await dependencyGraph.compare({
|
||||
@@ -57,7 +58,8 @@ async function run(): Promise<void> {
|
||||
filteredChanges,
|
||||
{
|
||||
allow: config.allow_licenses,
|
||||
deny: config.deny_licenses
|
||||
deny: config.deny_licenses,
|
||||
licenseExclusions: config.allow_dependencies_licenses
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ export const ConfigurationOptionsSchema = z
|
||||
fail_on_scopes: z.array(z.enum(SCOPES)).default(['runtime']),
|
||||
allow_licenses: z.array(z.string()).optional(),
|
||||
deny_licenses: z.array(z.string()).optional(),
|
||||
allow_dependencies_licenses: z.array(z.string()).optional(),
|
||||
allow_ghsas: z.array(z.string()).default([]),
|
||||
license_check: z.boolean().default(true),
|
||||
vulnerability_check: z.boolean().default(true),
|
||||
|
||||
@@ -143,6 +143,13 @@ export function addLicensesToSummary(
|
||||
`<strong>Denied Licenses</strong>: ${config.deny_licenses.join(', ')}`
|
||||
)
|
||||
}
|
||||
if (config.allow_dependencies_licenses) {
|
||||
core.summary.addQuote(
|
||||
`<strong>Excluded from license check</strong>: ${config.allow_dependencies_licenses.join(
|
||||
', '
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
core.debug(
|
||||
`found ${invalidLicenseChanges.unlicensed.length} unknown licenses`
|
||||
|
||||
Reference in New Issue
Block a user