-
Notifications
You must be signed in to change notification settings - Fork 4
174 lines (158 loc) · 6.6 KB
/
pr-scan.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# Copyright 2023 Circle Internet Financial, LTD. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
name: Pull Request Scan
on:
workflow_call:
inputs:
fail_on_severity:
description: |
Threshold for severity level which will trigger pipeline failure.
Possible values: low, moderate, high, critical.
required: false
type: string
default: critical
runs-on:
description: |
Github runner on which this workflow should run.
required: false
type: string
default: ubuntu-latest
allow-reciprocal-licenses:
description: |
(Only applicable to private repositories)
Allows licenses classified as 'reciprocal' to be used.
required: false
type: boolean
default: true
jobs:
scan:
runs-on: ${{ inputs.runs-on }}
steps:
- name: Setup
shell: bash
run: |-
# Can't use RUNNER_TEMP because actions/checkout requires the path be inside the workdir
SCAN_TEMP=$(mktemp -d scan-temp.XXXXX)
echo "SCAN_TEMP=${SCAN_TEMP}" >> ${GITHUB_ENV}
# install js-yaml and spdx-expression-parse
npm i js-yaml spdx-expression-parse
- name: Checkout Source
uses: actions/checkout@v4
with:
path: ${{ env.SCAN_TEMP }}/src
- name: Checkout Configs
uses: actions/checkout@v4
with:
repository: circlefin/circle-public-github-workflows
ref: stable
path: ${{ env.SCAN_TEMP }}/scan
sparse-checkout: |-
config/scan
- name: Configure dependency review
uses: actions/github-script@v7
id: config
env:
SEVERITY: ${{ inputs.fail_on_severity }}
ALLOW_RECIPROCAL_LICENSES: ${{ inputs.allow-reciprocal-licenses }}
with:
script: |-
const path = require('path');
const fs = require('fs');
const yaml = require('js-yaml');
const spdxParse = require('spdx-expression-parse');
const configsDir = path.join(process.env.SCAN_TEMP, 'scan', 'config', 'scan');
const srcDir = path.join(process.env.SCAN_TEMP, 'src');
if (context.eventName.startsWith("pull_request")) {
core.setOutput("is-pr", "true");
}
const validSeverities = ['low', 'moderate', 'high', 'critical'];
let severity = process.env.SEVERITY;
if (!validSeverities.includes(severity)) {
core.warning(`Invalid value for 'fail_on_severity': ${severity}`);
severity = 'low';
}
const privateRepo = context.payload?.repository?.private || false;
const isReciprocalLicenseAllowed = process.env.ALLOW_RECIPROCAL_LICENSES === 'true';
let licenseCfgFile = path.join(configsDir, 'license.yaml');
try {
const licenseCfg = yaml.load(fs.readFileSync(licenseCfgFile));
const allowedLicensesRaw = [];
allowedLicensesRaw.push(...licenseCfg.license.notice);
if (privateRepo && isReciprocalLicenseAllowed) {
allowedLicensesRaw.push(...licenseCfg.license.reciprocal);
}
// Filter out non-SPDX licenses
const allowedLicenses = allowedLicensesRaw.filter(l => {
try {
spdxParse(l);
return true;
} catch (_) {
return false;
}
});
let ignoredPackages = [];
const licenseIgnoreFile = path.join(srcDir, '.licenseignore');
try {
ignoredPackages = fs.readFileSync(licenseIgnoreFile).toString('utf-8').split('\n').filter(l => l.startsWith('pkg:'));
} catch (err) {
core.info(`Failed to read .licenseignore file: ${err}`);
}
let ignoredGhsas = [];
const ghsaIgnoreFile = path.join(srcDir, '.ghsaignore');
try {
for ( ghsa of fs.readFileSync(ghsaIgnoreFile).toString('utf-8').split('\n').filter(l => l.startsWith('GHSA-')) ){
try {
const ghsaTokens = ghsa.split(/\s+/);
if ( len(ghsaTokens) !== 2 ) {
throw new Error("Must specify GHSA ID and expiration date");
}
const expiration = Date.parse(ghsaTokens[1].replace(/^exp:/,''));
if (expiration > Date.now()) {
ignoredGhsas.push(ghsaTokens[0]);
}
} catch (err) {
core.warning(`Failed to parse line "${ghsa}" in .ghsaignore: ${err}`);
}
}
} catch (err) {
core.info(`Failed to read .ghsasignore file: ${err}`);
}
const depReviewCfg = {
'vulnerability-check': true,
'fail-on-severity': severity,
'license-check': true,
'fail-on-scopes': ['runtime', 'development', 'unknown'],
'comment-summary-in-pr': 'always',
'allow-licenses': allowedLicenses,
'allow-dependencies-licenses': ignoredPackages,
'allow-ghsas': ignoredGhsas,
}
licenseCfgFile = path.join(process.env.RUNNER_TEMP, 'dep-review.yaml');
const yamlConfig = yaml.dump(depReviewCfg);
core.debug(yamlConfig);
fs.writeFileSync(licenseCfgFile, yamlConfig);
} catch (err) {
core.error(`Failed to generate dependency review config file: ${err}`);
core.setFailed(err);
}
core.setOutput("config-file", licenseCfgFile);
- name: Pull Request Dependency license check
uses: actions/dependency-review-action@v4
if: >
steps.config.outcome == 'success' &&
steps.config.outputs.is-pr == 'true'
with:
config-file: ${{ steps.config.outputs.config-file }}