Skip to content

Added gradle-setup to utilize cache #2

Added gradle-setup to utilize cache

Added gradle-setup to utilize cache #2

Workflow file for this run

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 }}