Added gradle-setup to utilize cache #2
Workflow file for this run
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: 'Entur/Security/Code Scan' | |
on: | |
workflow_call: | |
env: | |
GITHUB_REPOSITORY: ${{ github.repository }} | |
jobs: | |
upload-scan-reports-from-matching-pr: | |
runs-on: ubuntu-latest | |
outputs: | |
skip_job_and_continue_scan: ${{ env.GHA_SECURITY_CODE_SCAN_SKIP_JOB_AND_CONTINUE_SCAN }} | |
permissions: | |
pull-requests: read | |
actions: read | |
security-events: write | |
contents: read | |
steps: | |
- name: "Skip job if not triggered by a push to main/master" | |
run: | | |
python -c " | |
import os | |
skip_job_and_continue_scan = False | |
env_file = os.getenv('GITHUB_ENV') | |
github_event_name = os.getenv('GITHUB_EVENT_NAME') | |
github_ref = os.getenv('GITHUB_REF') | |
if github_event_name != 'push' or (github_ref != 'refs/heads/main' and github_ref != 'refs/heads/master'): | |
skip_job_and_continue_scan = True | |
with open(env_file, 'a') as f: | |
f.write('GHA_SECURITY_CODE_SCAN_SKIP_JOB_AND_CONTINUE_SCAN=' + str(skip_job_and_continue_scan) + '\n') | |
" | |
env: | |
GITHUB_EVENT_NAME: ${{ github.event_name }} | |
GITHUB_REF: ${{ github.ref }} | |
- name: "Get workflow run ID for matching PR" | |
if: env.GHA_SECURITY_CODE_SCAN_SKIP_JOB_AND_CONTINUE_SCAN == 'False' | |
run: | | |
python -c " | |
import os | |
import requests | |
skip_job_and_continue_scan = False | |
github_sha = os.getenv('GITHUB_SHA') | |
github_token = os.getenv('GITHUB_TOKEN') | |
github_repository = os.getenv('GITHUB_REPOSITORY') | |
env_file = os.getenv('GITHUB_ENV') | |
headers = { | |
'Accept': 'application/vnd.github+json', | |
'X-GitHub-Api-Version': '2022-11-28', | |
'Authorization': f'token {github_token}' | |
} | |
five_recently_updated_closed_prs = requests.get( | |
f'https://api.github.com/repos/{github_repository}/pulls', | |
headers=headers, | |
params={ | |
'state': 'closed', | |
'per_page': '5', | |
'sort': 'updated', | |
'direction': 'desc' | |
} | |
) | |
if five_recently_updated_closed_prs.status_code != 200: | |
print('::error ::Error fetching closed PRs') | |
skip_job_and_continue_scan = True | |
else: | |
five_recently_updated_closed_prs = five_recently_updated_closed_prs.json() | |
pr_head = None | |
for closed_pr in five_recently_updated_closed_prs: | |
merge_commit_sha = closed_pr.get('merge_commit_sha') | |
if merge_commit_sha == github_sha: | |
pr_head = closed_pr.get('head').get('sha') | |
break | |
if pr_head != None: | |
workflow_runs = requests.get( | |
f'https://api.github.com/repos/{github_repository}/actions/runs', | |
headers=headers, | |
params={ | |
'head_sha': pr_head, | |
'event': 'pull_request', | |
'status': 'completed' | |
} | |
) | |
if workflow_runs.status_code != 200: | |
print('::error ::Error fetching workflow run') | |
skip_job_and_continue_scan = True | |
else: | |
workflow_runs = workflow_runs.json().get('workflow_runs', []) | |
run_id = None | |
for workflow_run in workflow_runs: | |
if workflow_run.get('path') == '.github/workflows/codeql.yml': | |
run_id = workflow_run.get('id') | |
with open(env_file, 'a') as f: | |
f.write(f'GHA_SECURITY_CODE_SCAN_WORKFLOW_RUN_ID={run_id}\n') | |
break | |
if run_id is None: | |
print('::notice ::No matching workflow run found') | |
skip_job_and_continue_scan = True | |
else: | |
print('::notice ::No matching PR found') | |
skip_job_and_continue_scan = True | |
with open(env_file, 'a') as f: | |
f.write('GHA_SECURITY_CODE_SCAN_SKIP_JOB_AND_CONTINUE_SCAN=' + str(skip_job_and_continue_scan) + '\n') | |
" | |
env: | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
GITHUB_SHA: ${{ github.sha }} | |
- name: "Download existing scan reports from workflow run ID" | |
if: env.GHA_SECURITY_CODE_SCAN_SKIP_JOB_AND_CONTINUE_SCAN == 'False' | |
uses: actions/download-artifact@v4 | |
with: | |
run-id: ${{ env.GHA_SECURITY_CODE_SCAN_WORKFLOW_RUN_ID }} | |
github-token: ${{ secrets.GITHUB_TOKEN }} | |
path: "/home/runner/work/${{ env.GITHUB_REPOSITORY }}/results" | |
- name: "Upload scan reports to Security tab" | |
if: env.GHA_SECURITY_CODE_SCAN_SKIP_JOB_AND_CONTINUE_SCAN == 'False' | |
uses: github/codeql-action/upload-sarif@v3 | |
with: | |
sarif_file: "/home/runner/work/${{ env.GITHUB_REPOSITORY }}/results" | |
category: 'scan-results' | |
get-repository-languages: | |
if: github.event_name == 'pull_request' || (github.event_name == 'push' && needs.upload-scan-reports-from-matching-pr.outputs.skip_job_and_continue_scan == 'True') | |
runs-on: ubuntu-latest | |
needs: upload-scan-reports-from-matching-pr | |
outputs: | |
repository_languages: ${{ env.GHA_SECURITY_CODE_SCAN_REPOSITORY_LANGUAGES }} | |
scala_found: ${{ env.GHA_SECURITY_CODE_SCAN_SCALA_FOUND }} | |
steps: | |
- name: "Get repository languages" | |
id: get-repository-languages | |
run: | | |
repo_languages=$(gh api /repos/${{ env.GITHUB_REPOSITORY }}/languages) | |
languages=$(echo "$repo_languages" | \ | |
jq 'keys | |
| .[] as $langs | |
| { | |
"C":"c", | |
"C++":"cpp", | |
"C#":"csharp", | |
"Go":"go", | |
"Java":"java", | |
"JavaScript":"javascript-typescript", | |
"TypeScript":"javascript-typescript", | |
"Kotlin":"kotlin", | |
"Python":"python", | |
"Ruby":"ruby", | |
"Swift":"swift" | |
} as $supported | |
| $langs # operate on all the languages | |
| $supported[.] # and lookup their values, null if not found | |
| select(.) # select removes null values | |
' | \ | |
jq --slurp --compact-output 'unique') # make a oneliner, and remove duplicates | |
scala_found=$(echo "$repo_languages" | jq 'keys | .[]' | grep -q 'Scala' && echo 'true' || echo 'false') | |
echo 'GHA_SECURITY_CODE_SCAN_SCALA_FOUND='$scala_found >> $GITHUB_ENV | |
echo 'GHA_SECURITY_CODE_SCAN_REPOSITORY_LANGUAGES='$languages >> $GITHUB_ENV | |
env: | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
semgrep-analysis: | |
if: needs.get-repository-languages.outputs.scala_found == 'true' | |
runs-on: ubuntu-latest | |
needs: get-repository-languages | |
steps: | |
- name: "Checkout repository" | |
uses: actions/checkout@v4 | |
- name: "Set up Semgrep" | |
run: | | |
python3 -m pip install semgrep | |
- name: "Scan with Semgrep" | |
run: | | |
semgrep scan --sarif \ | |
--sarif-output=semgrep.sarif \ | |
--no-secrets-validation \ | |
--metrics=off \ | |
--config=p/scala \ | |
--exclude=*.c \ | |
--exclude=*.h \ | |
--exclude=*.cpp \ | |
--exclude=*.cxx \ | |
--exclude=*.cc \ | |
--exclude=*.hxx \ | |
--exclude=*.cs \ | |
--exclude=*.go \ | |
--exclude=*.java \ | |
--exclude=*.js \ | |
--exclude=*.ts \ | |
--exclude=*.kt \ | |
--exclude=*.py \ | |
--exclude=*.rb \ | |
--exclude=*.swift | |
- name: "Upload Semgrep report" | |
id: upload-semgrep-report | |
uses: github/codeql-action/upload-sarif@v3 | |
with: | |
sarif_file: semgrep.sarif | |
category: 'semgrep-scan' | |
- name: "Upload Semgrep report as artifact" | |
uses: actions/upload-artifact@v4 | |
with: | |
name: semgrep-sarif | |
path: semgrep.sarif | |
overwrite: true | |
codeql-analysis: | |
if: needs.get-repository-languages.outputs.repository_languages != '[]' | |
runs-on: ubuntu-latest | |
needs: get-repository-languages | |
permissions: | |
# CodeQL - required for all workflows | |
security-events: write | |
# CodeQL - only required for workflows in private repositories | |
actions: read | |
contents: read | |
strategy: | |
fail-fast: false | |
matrix: | |
language: ${{fromJson(needs.get-repository-languages.outputs.repository_languages)}} | |
steps: | |
- name: "Checkout repository" | |
uses: actions/checkout@v4 | |
- name: "Set up Gradle for Java/Kotlin" | |
if: matrix.language == 'kotlin' || matrix.language == 'java' | |
uses: gradle/actions/setup-gradle | |
with: | |
cache-read-only: true # Force read only, | |
- name: "Initialize CodeQL for Java/Kotlin" | |
if: matrix.language == 'kotlin' || matrix.language == 'java' | |
uses: github/codeql-action/init@v3 | |
with: | |
languages: ${{ matrix.language }} | |
build-mode: autobuild | |
- name: "Initialize CodeQL" | |
if: matrix.language != 'java' && matrix.language != 'kotlin' | |
uses: github/codeql-action/init@v3 | |
with: | |
languages: ${{ matrix.language }} | |
- name: "Perform CodeQL Analysis" | |
id: codeql-analysis | |
uses: github/codeql-action/analyze@v3 | |
with: | |
category: "/language:${{ matrix.language }}" | |
env: | |
ARTIFACTORY_USER: ${{ secrets.ARTIFACTORY_USER }} | |
ARTIFACTORY_APIKEY: ${{ secrets.ARTIFACTORY_APIKEY }} | |
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx2g | |
- name: "Get repository name" | |
run: | | |
REPOSITORY_NAME=$(echo "${{ env.GITHUB_REPOSITORY }}" | cut -d'/' -f2) | |
echo "REPOSITORY_NAME=$REPOSITORY_NAME" >> $GITHUB_ENV | |
- name: "Upload CodeQL report as artifact" | |
uses: actions/upload-artifact@v4 | |
with: | |
name: codeql-${{ matrix.language }} | |
path: "/home/runner/work/${{ env.REPOSITORY_NAME }}/results" | |
overwrite: true | |
allowlist-code-scan-alerts: | |
needs: [codeql-analysis, semgrep-analysis] | |
runs-on: ubuntu-latest | |
permissions: | |
# required to read and update code scan alerts | |
security-events: write | |
# required to checkout the repository | |
contents: read | |
steps: | |
- name: Install dependencies | |
run: | | |
cat > requirements.txt << EOF | |
Cerberus == 1.3.5 --hash=sha256:7649a5815024d18eb7c6aa5e7a95355c649a53aacfc9b050e9d0bf6bfa2af372 --hash=sha256:81011e10266ef71b6ec6d50e60171258a5b134d69f8fb387d16e4936d0d47642 | |
EOF | |
pip install --only-binary=:all: --require-hashes -r requirements.txt | |
rm requirements.txt | |
shell: bash | |
- name: "Checkout repository" | |
uses: actions/checkout@v4 | |
- name: "Check if allowlist files exist" | |
run: | | |
python -c " | |
import os | |
import requests | |
import base64 | |
import yaml | |
external_token = os.getenv('GHA_SECURITY_CODE_SCAN_EXTERNAL_REPOSITORY_TOKEN') | |
env_file = os.getenv('GITHUB_ENV') | |
external_repository = '' | |
allowlist = False | |
external_allowlist = False | |
for file_name in ('code_scan_config.yml', 'code_scan_config.yaml'): | |
try: | |
with open(file_name, 'r') as f: | |
external_repository = yaml.safe_load(f).get('spec', {}).get('inherit', '') | |
allowlist = True | |
break | |
except IOError: | |
pass | |
if allowlist == False: | |
print(f'::notice ::Allowlist file not found') | |
if external_repository != '': | |
headers = { | |
'Accept': 'application/vnd.github+json', | |
'X-GitHub-Api-Version': '2022-11-28', | |
'Authorization': f'token {external_token}' | |
} | |
for file_name in ('code_scan_config.yml', 'code_scan_config.yaml'): | |
response = requests.get(f'https://api.github.com/repos/entur/{external_repository}/contents/{file_name}', headers=headers) | |
if response.status_code != 404 and response.status_code != 200: | |
print(f'::error ::Could not get external allowlist file ({response.status_code} - {response.reason})') | |
break | |
if response.status_code == 200: | |
external_allowlist = True | |
break | |
if external_allowlist == False: | |
print(f'::notice ::External allowlist file not found') | |
if external_allowlist: | |
with open('external_code_scan_config.yml', 'w') as f: | |
f.write(base64.b64decode(response.json().get('content','')).decode('utf-8')) | |
with open(env_file, 'a') as f: | |
f.write(f'GHA_SECURITY_CODE_SCAN_ALLOWLIST={allowlist}\n') | |
f.write(f'GHA_SECURITY_CODE_SCAN_EXTERNAL_ALLOWLIST={external_allowlist}\n') | |
" | |
env: | |
GHA_SECURITY_CODE_SCAN_EXTERNAL_REPOSITORY_TOKEN: ${{ secrets.external_repository_token }} | |
- name: "Validate allowlist file(s)" | |
id: validate_allowlists | |
if: ${{ env.GHA_SECURITY_CODE_SCAN_ALLOWLIST == 'True' }} | |
run: | | |
python -c " | |
import yaml | |
from cerberus import Validator | |
import sys | |
import os | |
schema = { | |
'apiVersion': {'type': 'string', 'required': True}, | |
'kind': {'type': 'string', 'required': True, 'allowed': ['CodeScanConfig']}, | |
'metadata': { | |
'type': 'dict', | |
'required': True, | |
'schema': { | |
'name': {'type': 'string', 'required': True} | |
} | |
}, | |
'spec': { | |
'type': 'dict', | |
'required': True, | |
'schema': { | |
'inherit': {'type': 'string' }, | |
'allowlist': { | |
'type': 'list', | |
'minlength': 1, | |
'schema': { | |
'type': 'dict', | |
'schema': { | |
'cwe': {'type': 'string', 'required': True}, | |
'comment': {'type': 'string', 'required': True}, | |
'reason': {'type': 'string', 'required': True} | |
} | |
} | |
} | |
} | |
} | |
} | |
v = Validator(schema) | |
data = '' | |
for file_name in ('code_scan_config.yml', 'code_scan_config.yaml'): | |
try: | |
with open(file_name, 'r') as f: | |
data = yaml.safe_load(f) | |
break | |
except IOError: | |
pass | |
if data == '': | |
print('::error ::Allowlist file not found') | |
sys.exit(1) | |
if v.validate(data): | |
print('The allowlist file is valid.') | |
else: | |
print(f'::error ::The allowlist file is invalid. Here are the errors: {v.errors}') | |
sys.exit(1) | |
if os.getenv('GHA_SECURITY_CODE_SCAN_EXTERNAL_ALLOWLIST') == 'True': | |
data = '' | |
try: | |
with open('external_code_scan_config.yml', 'r') as f: | |
data = yaml.safe_load(f) | |
except IOError: | |
print('::error ::External allowlist file not found') | |
sys.exit(1) | |
if v.validate(data): | |
print('The external allowlist file is valid.') | |
else: | |
print(f'::error ::The external allowlist file is invalid. Here are the errors: {v.errors}') | |
sys.exit(1) | |
" | |
- name: "Allowlist code scan alerts" | |
if: steps.validate_allowlists.outcome == 'success' | |
run: | | |
python -c " | |
import requests | |
import yaml | |
import json | |
import os | |
import sys | |
import time | |
repository = os.getenv('GITHUB_REPOSITORY') | |
token = os.getenv('GITHUB_TOKEN') | |
headers = { | |
'Accept': 'application/vnd.github+json', | |
'X-GitHub-Api-Version': '2022-11-28', | |
'Authorization': f'token {token}' | |
} | |
allowlist = {} | |
reason_mapping = { | |
'false_positive': 'false positive', | |
'wont_fix': 'won\'t fix', | |
'test': 'used in tests' | |
} | |
try: | |
with open('external_code_scan_config.yml', 'r') as f: | |
remote_config = yaml.safe_load(f) | |
for element in remote_config.get('spec', {}).get('allowlist', []): | |
cwe = element.get('cwe', '') | |
cwe_tag = f'external/cwe/{cwe}' | |
allowlist[cwe_tag] = { | |
'comment': element.get('comment', ''), | |
'reason': reason_mapping[element.get('reason', '')] | |
} | |
except IOError: | |
pass | |
for file_name in ('code_scan_config.yml', 'code_scan_config.yaml'): | |
try: | |
with open(file_name, 'r') as f: | |
local_config = yaml.safe_load(f) | |
for element in local_config.get('spec', {}).get('allowlist', []): | |
cwe = element.get('cwe', '') | |
cwe_tag = f'external/cwe/{cwe}' | |
allowlist[cwe_tag] = { | |
'comment': element.get('comment', ''), | |
'reason': reason_mapping[element.get('reason', '')] | |
} | |
break | |
except IOError: | |
pass | |
url = f'https://api.github.com/repos/{repository}/code-scanning/alerts?ref=${{ github.ref }}&per_page=100&state=open' | |
while True: | |
response = requests.get(url, headers=headers) | |
if response.status_code != 200: | |
print(f'::error ::Could not get code scan alerts ({response.status_code} - {response.reason})') | |
sys.exit(1) | |
alerts = response.json() | |
for alert in alerts: | |
for cwe_tag, data in allowlist.items(): | |
if cwe_tag in alert.get('rule', {}).get('tags', []): | |
alert_number = alert.get('number', '') | |
patch_url = f'https://api.github.com/repos/{repository}/code-scanning/alerts/{alert_number}' | |
data_to_send = { | |
'state': 'dismissed', | |
'dismissed_reason': data.get('reason', ''), | |
'dismissed_comment': data.get('comment', '') | |
} | |
while True: | |
response_patch = requests.patch(patch_url, headers=headers, json=data_to_send) | |
x_ratelimit_remaining = response_patch.headers.get('x-ratelimit-remaining') | |
if (response_patch.status_code == 403 or response_patch.status_code == 429) and x_ratelimit_remaining == '0': | |
x_ratelimit_reset = response_patch.headers.get('x-ratelimit-reset') | |
current_time = int(time.time()) | |
reset_time = int(x_ratelimit_reset) - current_time | |
time.sleep(reset_time) | |
else: | |
break | |
links = response.headers.get('Link', '') | |
link_next_j = links.find('>; rel=\"next\"') | |
if link_next_j == -1: | |
break | |
link_next_i = links.rfind('<', 0, link_next_j) | |
if link_next_i == -1: | |
break | |
url = links[link_next_i + 1:link_next_j] | |
" | |
env: | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
check-for-critical-alerts: | |
runs-on: ubuntu-latest | |
if: always() && github.event_name == 'pull_request' | |
needs: [allowlist-code-scan-alerts] | |
permissions: | |
security-events: read # required to read code scan alerts | |
issues: write # required for creating issues, and/or adding issue comments | |
pull-requests: write # required for creating comments on pull requests | |
steps: | |
- name: "Get critical code scan alerts" | |
run: | | |
alerts="$(gh api \ | |
--method GET \ | |
-H 'Accept: application/vnd.github+json' \ | |
-H 'X-GitHub-Api-Version: 2022-11-28' \ | |
/repos/${{ env.GITHUB_REPOSITORY }}/code-scanning/alerts \ | |
-F severity='critical' -F state='open' -F ref='${{ github.ref }}' -F per_page='100' -F tool_name='CodeQL' --paginate)" | |
if [ "$alerts" == "[]" ]; then | |
echo 'GHA_SECURITY_CODE_SCAN_CREATE_PR_COMMENT='False >> $GITHUB_ENV | |
else | |
echo 'GHA_SECURITY_CODE_SCAN_CREATE_PR_COMMENT='True >> $GITHUB_ENV | |
fi | |
env: | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
- name: "Create comment on pull request if critical alerts are found" | |
if: env.GHA_SECURITY_CODE_SCAN_CREATE_PR_COMMENT == 'True' | |
run: | | |
gh api /repos/${{ env.GITHUB_REPOSITORY }}/issues/${{ github.event.pull_request.number }}/comments \ | |
-H "Accept: application/vnd.github.v3+json" \ | |
--field "body=:no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry: | |
Code scan detected critical vulnerabilities in the code. Please address these vulnerabilities as soon as possible. | |
The scan results can be found [here](https://github.com/${{ env.GITHUB_REPOSITORY }}/security/code-scanning?query=is%3Aopen+pr%3A${{ github.event.pull_request.number }}). | |
If you believe one or more of the reported vulnerabilities are false positives/cannot be fixed/can be ignored, please see the [Code Scan documentation](https://github.com/entur/gha-security/blob/main/README-code_scan.md#white-listing-vulnerabilities) on how to allowlist. | |
:no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry: :no_entry:" | |
env: | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |