Skip to content

released-cve-audit

released-cve-audit #41

name: released-cve-audit
on:
schedule:
# Run after the nightly release window so newly published CVEs are noticed daily.
- cron: "15 19 * * *"
workflow_dispatch:
inputs:
release_count:
description: "Number of latest channel releases to scan when release_sequences is blank"
required: false
type: string
default: "3"
channel:
description: "Replicated channel to audit"
required: false
type: string
default: "Stable"
release_sequences:
description: "Optional comma-separated Replicated release sequences to scan instead of resolving by channel"
required: false
type: string
default: ""
notify_slack:
description: "Send the final audit summary to Slack"
required: false
type: boolean
default: true
permissions:
contents: read
env:
AWS_REGION: us-east-1
ECR_REGISTRY: 008971668139.dkr.ecr.us-east-1.amazonaws.com
GRYPE_VERSION: v0.115.0
OPRA_CI_DOPPLER_IDENTITY_ID: fc94b8db-1149-4b8a-8ba7-1fd4833bb870
SLACK_IMPLEMENTATIONS_USERGROUP_ID: S092C0T771V
jobs:
audit:
runs-on: ubuntu-latest
timeout-minutes: 180
permissions:
id-token: write
contents: read
steps:
- name: Fetch audit secrets from Doppler
id: doppler
uses: dopplerhq/secrets-fetch-action@451892f16195f9ac360e1a5bcbf0b5fd0e957534
with:
auth-method: oidc
doppler-identity-id: ${{ env.OPRA_CI_DOPPLER_IDENTITY_ID }}
doppler-project: opra-ci
doppler-config: prd
inject-env-vars: true
- name: Validate audit secrets
shell: bash
env:
NOTIFY_SLACK: ${{ github.event_name != 'workflow_dispatch' || inputs.notify_slack }}
run: |
set -euo pipefail
missing=()
for name in REPLICATED_API_TOKEN REPLICATED_APP; do
if [[ -z "${!name:-}" ]]; then
missing+=("${name}")
fi
done
if [[ "${NOTIFY_SLACK}" == "true" && -z "${NIGHTLY_SLACK_WEBHOOK_URL:-}" ]]; then
missing+=("NIGHTLY_SLACK_WEBHOOK_URL")
fi
if (( ${#missing[@]} > 0 )); then
printf 'Missing required Doppler secret(s): %s\n' "${missing[*]}" >&2
exit 1
fi
- name: Checkout CVE audit scripts
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
sparse-checkout: |
.github/scripts/cve
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Install Helm
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4
with:
version: 'latest'
- name: Install yq
uses: mikefarah/yq@1b9b4ac5187171d2e5e3129be0cfa827c7f9d53d
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a
with:
role-to-assume: arn:aws:iam::008971668139:role/github-actions-helm-charts-ecr
aws-region: ${{ env.AWS_REGION }}
- name: Login to ECR for Grype registry scans
shell: bash
run: |
set -euo pipefail
aws ecr get-login-password --region "${AWS_REGION}" \
| docker login --username AWS --password-stdin "${ECR_REGISTRY}"
- name: Install Grype
shell: bash
run: |
set -euo pipefail
curl -sSfL https://get.anchore.io/grype -o /tmp/install-grype.sh
sudo sh /tmp/install-grype.sh -b /usr/local/bin "${GRYPE_VERSION}"
grype version
- name: Install Replicated CLI
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
curl -fsSL \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/replicatedhq/replicated/releases/latest \
-o replicated-release.json
version="$(jq -r '.tag_name | sub("^v"; "")' replicated-release.json)"
curl -fsSL \
"https://github.com/replicatedhq/replicated/releases/download/v${version}/replicated_${version}_linux_amd64.tar.gz" \
-o replicated.tar.gz
tar xf replicated.tar.gz replicated
sudo install replicated /usr/local/bin/replicated
- name: Resolve releases to audit
id: releases
shell: bash
env:
CHANNEL: ${{ inputs.channel || 'Stable' }}
RELEASE_COUNT: ${{ inputs.release_count || '3' }}
RELEASE_SEQUENCES: ${{ inputs.release_sequences || '' }}
run: |
set -euo pipefail
mkdir -p audit-reports
if ! [[ "${RELEASE_COUNT}" =~ ^[0-9]+$ ]]; then
echo "release_count must be a positive integer" >&2
exit 1
fi
if [[ -n "${RELEASE_SEQUENCES//[[:space:],]/}" ]]; then
printf '%s\n' "${RELEASE_SEQUENCES}" \
| tr ',' '\n' \
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
| grep -E '^[0-9]+$' \
| jq -R -s 'split("\n") | map(select(. != "")) | map({sequence: (tonumber), channels: ["manual"], created_at: "", chart_version: ""})' \
> audit-reports/releases.json
else
replicated api get /v3/apps \
--app "${REPLICATED_APP}" \
--token "${REPLICATED_API_TOKEN}" \
> audit-reports/replicated-apps.raw.json
sed -n '/^{/,$p' audit-reports/replicated-apps.raw.json > audit-reports/replicated-apps.json
app_id="$(
jq -r \
--arg app "${REPLICATED_APP}" \
'.apps[]?
| select(.slug == $app or .id == $app or .name == $app)
| .id' \
audit-reports/replicated-apps.json \
| head -n1
)"
channel_id="$(
jq -r \
--arg app "${REPLICATED_APP}" \
--arg channel "${CHANNEL}" \
'.apps[]?
| select(.slug == $app or .id == $app or .name == $app)
| .channels[]?
| select(.name == $channel or .channelSlug == ($channel | ascii_downcase) or .id == $channel)
| .id' \
audit-reports/replicated-apps.json \
| head -n1
)"
if [[ -z "${app_id}" || -z "${channel_id}" ]]; then
echo "Could not resolve Replicated app/channel for app=${REPLICATED_APP} channel=${CHANNEL}" >&2
exit 1
fi
replicated api get "/v3/app/${app_id}/channel/${channel_id}/releases" \
--app "${REPLICATED_APP}" \
--token "${REPLICATED_API_TOKEN}" \
> audit-reports/replicated-channel-releases.raw.json
sed -n '/^{/,$p' audit-reports/replicated-channel-releases.raw.json > audit-reports/replicated-channel-releases.json
jq \
--arg channel "${CHANNEL}" \
--argjson limit "${RELEASE_COUNT}" \
'(.releases // [])
| sort_by(.channelSequence // 0)
| reverse
| .[:$limit]
| map({
sequence,
created_at: (.releasedAt // .created // .createdAt // ""),
channels: [(.channelName // $channel)],
chart_version: ((.chartReleases[]? | select(.name == "composio") | .version) // .semver // .version // ""),
channel_sequence: (.channelSequence // null)
})' \
audit-reports/replicated-channel-releases.json \
> audit-reports/releases.json
fi
jq empty audit-reports/releases.json
release_total="$(jq 'length' audit-reports/releases.json)"
if (( release_total == 0 )); then
echo "No releases resolved for CVE audit." >&2
exit 1
fi
echo "Resolved releases:"
jq -r '.[] | "- sequence \(.sequence) channel-sequence=\(.channel_sequence // "manual") channels=\(.channels | join(",")) chart=\(.chart_version // "unknown")"' audit-reports/releases.json
- name: Scan released charts
id: audit
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
CHANNEL: ${{ inputs.channel || 'Stable' }}
run: |
set -euo pipefail
scan_started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
artifact_name="released-grype-cve-audit-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
artifact_download_command="gh run download ${GITHUB_RUN_ID} --repo ${GITHUB_REPOSITORY} --name ${artifact_name} --dir ./released-cve-audit"
echo '[]' > audit-reports/release-results.json
mapfile -t releases < <(jq -c '.[]' audit-reports/releases.json)
for release in "${releases[@]}"; do
sequence="$(jq -r '.sequence' <<<"${release}")"
created_at="$(jq -r '.created_at // ""' <<<"${release}")"
release_dir="audit-reports/release-${sequence}"
mkdir -p "${release_dir}/chart" "${release_dir}/baseline"
echo "Inspecting Replicated release sequence ${sequence}"
replicated release inspect "${sequence}" \
--output json \
--app "${REPLICATED_APP}" \
--token "${REPLICATED_API_TOKEN}" \
> "${release_dir}/replicated-release.raw.json"
sed -n '/^{/,$p' "${release_dir}/replicated-release.raw.json" > "${release_dir}/replicated-release.json"
chart_version="$(
jq -r '(.charts[]? | select(.name == "composio") | .version) // ""' "${release_dir}/replicated-release.json"
)"
chart_content="$(
jq -r 'first(.config | fromjson[] | select(.name | test("^composio-[^/]+[.]tgz$")) | .content) // ""' \
"${release_dir}/replicated-release.json"
)"
if [[ -z "${chart_content}" || "${chart_content}" == "null" ]]; then
jq -n \
--arg sequence "${sequence}" \
--arg created_at "${created_at}" \
--arg chart_version "${chart_version}" \
--arg error "Replicated release did not include a composio chart archive" \
'{
scanResult: "failed",
totalHigh: 0,
totalCritical: 0,
fixedNow: 0,
fixedAtReleaseScan: 0,
newlyDetected: 0,
findings: [],
imageStats: [],
release_sequence: ($sequence | tonumber),
release_created_at: $created_at,
release_chart_version: $chart_version,
detected_release_tag: "",
baseline_source: "missing",
error: $error
}' > "${release_dir}/report-result.json"
else
printf '%s' "${chart_content}" | base64 -d > "${release_dir}/composio.tgz"
tar -xzf "${release_dir}/composio.tgz" -C "${release_dir}/chart"
chart_root="$(find "${release_dir}/chart" -mindepth 1 -maxdepth 1 -type d | head -n1)"
CHART_DIR="${chart_root}" \
OUTPUT_DIR="${release_dir}" \
OVERRIDE_IMAGE_TAGS=0 \
RENDER_TEMPORAL=1 \
.github/scripts/cve/render-scan-targets.sh
detected_release_tag="$(
jq -r \
--arg prefix "${ECR_REGISTRY}/composio-self-host/" \
'[.[]?.scan_image
| select(startswith($prefix))
| capture(":(?<tag>[^:@]+)$").tag]
| group_by(.)
| sort_by(length)
| reverse
| .[0][0] // ""' \
"${release_dir}/scan-targets.json"
)"
baseline_file=""
baseline_source="missing"
for candidate in "${detected_release_tag}" "${chart_version}" "v${chart_version}"; do
if [[ -z "${candidate}" || "${candidate}" == "v" ]]; then
continue
fi
rm -f "${release_dir}/baseline/release-cve-baseline.json"
if gh release download "${candidate}" \
--repo "${GITHUB_REPOSITORY}" \
--pattern release-cve-baseline.json \
--dir "${release_dir}/baseline" \
>/dev/null 2>&1; then
baseline_file="${release_dir}/baseline/release-cve-baseline.json"
baseline_source="${candidate}"
break
fi
done
OUTPUT_DIR="${release_dir}" .github/scripts/cve/run-grype-scan.sh
baseline_args=()
if [[ -n "${baseline_file}" ]]; then
baseline_args=(--baseline-findings "${baseline_file}")
fi
node .github/scripts/cve/build-cve-report.mjs \
--reports-dir "${release_dir}" \
--scan-targets "${release_dir}/scan-targets.json" \
--output-dir "${release_dir}" \
--release-tag "${detected_release_tag:-replicated-${sequence}}" \
--release-version "${chart_version:-replicated-sequence-${sequence}}" \
--scan-started-at "${scan_started_at}" \
"${baseline_args[@]}" \
> "${release_dir}/report-result.raw.json"
jq \
--arg sequence "${sequence}" \
--arg created_at "${created_at}" \
--arg chart_version "${chart_version}" \
--arg detected_release_tag "${detected_release_tag}" \
--arg baseline_source "${baseline_source}" \
'. + {
release_sequence: ($sequence | tonumber),
release_created_at: $created_at,
release_chart_version: $chart_version,
detected_release_tag: $detected_release_tag,
baseline_source: $baseline_source
}' \
"${release_dir}/report-result.raw.json" \
> "${release_dir}/report-result.json"
fi
jq -c -s '.[0] + [.[1]]' \
audit-reports/release-results.json \
"${release_dir}/report-result.json" \
> audit-reports/release-results.tmp.json
mv audit-reports/release-results.tmp.json audit-reports/release-results.json
done
node .github/scripts/cve/build-released-cve-audit.mjs \
--release-results audit-reports/release-results.json \
--output-dir audit-reports \
--channel "${CHANNEL}" \
--scan-started-at "${scan_started_at}" \
--artifact-name "${artifact_name}" \
--artifact-download-command "${artifact_download_command}" \
--slack-usergroup-id "${SLACK_IMPLEMENTATIONS_USERGROUP_ID}"
cat audit-reports/summary.md >> "${GITHUB_STEP_SUMMARY}"
status="$(jq -r '.status' audit-reports/released-cve-audit-result.json)"
title="$(jq -r '.slackTitle' audit-reports/released-cve-audit-result.json)"
{
echo "status=${status}"
echo "title=${title}"
echo "artifact_name=${artifact_name}"
echo "artifact_download_command=${artifact_download_command}"
echo "summary<<__AUDIT_SUMMARY__"
cat audit-reports/slack-summary.md
echo "__AUDIT_SUMMARY__"
} >> "${GITHUB_OUTPUT}"
- name: Build released CVE audit Slack alert
id: alert
if: always()
shell: bash
env:
CHANNEL: ${{ inputs.channel || 'Stable' }}
RELEASE_COUNT: ${{ inputs.release_count || '3' }}
RELEASE_SEQUENCES: ${{ inputs.release_sequences || '' }}
RELEASES_OUTCOME: ${{ steps.releases.outcome }}
AUDIT_OUTCOME: ${{ steps.audit.outcome }}
ARTIFACT_NAME: ${{ steps.audit.outputs.artifact_name || format('released-grype-cve-audit-{0}-{1}', github.run_id, github.run_attempt) }}
ARTIFACT_DOWNLOAD_COMMAND: ${{ steps.audit.outputs.artifact_download_command || format('gh run download {0} --repo {1} --name released-grype-cve-audit-{0}-{2} --dir ./released-cve-audit', github.run_id, github.repository, github.run_attempt) }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
mkdir -p audit-reports
if [[ -f .github/scripts/cve/build-released-cve-audit-alert.mjs ]]; then
node .github/scripts/cve/build-released-cve-audit-alert.mjs \
--output-dir audit-reports \
--channel "${CHANNEL}" \
--release-count "${RELEASE_COUNT}" \
--release-sequences "${RELEASE_SEQUENCES}" \
--releases-outcome "${RELEASES_OUTCOME}" \
--audit-outcome "${AUDIT_OUTCOME}" \
--artifact-name "${ARTIFACT_NAME}" \
--artifact-download-command "${ARTIFACT_DOWNLOAD_COMMAND}" \
--run-url "${RUN_URL}" \
--slack-usergroup-id "${SLACK_IMPLEMENTATIONS_USERGROUP_ID}"
else
if [[ -n "${RELEASE_SEQUENCES}" ]]; then
requested="explicit Replicated release sequence(s): \`${RELEASE_SEQUENCES}\`"
else
requested="channel \`${CHANNEL}\`, latest ${RELEASE_COUNT} release(s)"
fi
jq -n \
--arg usergroup "${SLACK_IMPLEMENTATIONS_USERGROUP_ID}" \
--arg requested "${requested}" \
--arg releases_outcome "${RELEASES_OUTCOME}" \
--arg audit_outcome "${AUDIT_OUTCOME}" \
--arg artifact_name "${ARTIFACT_NAME}" \
--arg artifact_download_command "${ARTIFACT_DOWNLOAD_COMMAND}" \
--arg run_url "${RUN_URL}" \
'{
status: "FAILURE",
title: ":rotating_light: Released CVE audit: WORKFLOW FAILED",
summary: (
(if $usergroup == "" then "" else "<!subteam^\($usergroup)|@implementations>\n" end) +
"*Good to go:* NO - released CVE audit did not complete\n" +
"*Scope:* Already released channel versions only; independent of the nightly release workflow.\n" +
"*Release blocking:* NO - this audit is alert-only.\n" +
"*Requested audit:* \($requested)\n" +
"*Findings:* UNKNOWN - the audit failed before producing the CVE summary.\n" +
"*Stages:*\n" +
"- Release resolution: `\($releases_outcome)`\n" +
"- Audit scan/report: `\($audit_outcome)`\n" +
"*Artifact:* `\($artifact_name)`\n" +
"*Download partial reports, if any were uploaded:*\n" +
"```\n\($artifact_download_command)\n```\n" +
"*Workflow:* <\($run_url)|released CVE audit run>\n"
)
}' > audit-reports/released-cve-audit-alert.json
jq -r '.summary' audit-reports/released-cve-audit-alert.json > audit-reports/released-cve-audit-alert-summary.md
fi
status="$(jq -r '.status' audit-reports/released-cve-audit-alert.json)"
title="$(jq -r '.title' audit-reports/released-cve-audit-alert.json)"
{
echo "status=${status}"
echo "title=${title}"
echo "artifact_name=${ARTIFACT_NAME}"
echo "artifact_download_command=${ARTIFACT_DOWNLOAD_COMMAND}"
echo "summary<<__AUDIT_ALERT_SUMMARY__"
jq -r '.summary' audit-reports/released-cve-audit-alert.json
echo "__AUDIT_ALERT_SUMMARY__"
} >> "${GITHUB_OUTPUT}"
- name: Upload released CVE audit reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: ${{ steps.alert.outputs.artifact_name || steps.audit.outputs.artifact_name || format('released-grype-cve-audit-{0}-{1}', github.run_id, github.run_attempt) }}
if-no-files-found: warn
path: audit-reports
- name: Announce released CVE audit in Slack
if: ${{ always() && (github.event_name != 'workflow_dispatch' || inputs.notify_slack) && steps.alert.outputs.summary != '' && steps.doppler.outputs.NIGHTLY_SLACK_WEBHOOK_URL != '' }}
shell: bash
env:
INPUT_STATUS: ${{ steps.alert.outputs.status }}
INPUT_TITLE: ${{ steps.alert.outputs.title }}
INPUT_SUMMARY: ${{ steps.alert.outputs.summary }}
SLACK_WEBHOOK_URL: ${{ steps.doppler.outputs.NIGHTLY_SLACK_WEBHOOK_URL }}
run: |
set -euo pipefail
case "${INPUT_STATUS}" in
SUCCESS) color="#28a745" ;;
FAILURE) color="#cb2431" ;;
*) color="#808080" ;;
esac
payload="$(
jq -n \
--arg color "${color}" \
--arg title "${INPUT_TITLE}" \
--arg summary "${INPUT_SUMMARY}" \
'
def chunks($text; $size):
[range(0; ($text | length); $size) as $start | $text[$start:($start + $size)]]
| map(select(length > 0));
{
text: ($title + "\n" + $summary[0:3000]),
attachments: [{
color: $color,
blocks: ([
{
type: "section",
text: { type: "mrkdwn", text: ("*" + $title + "*") }
}
] + (chunks($summary; 2900) | .[:49] | map({
type: "section",
text: { type: "mrkdwn", text: . }
})))
}],
unfurl_links: false,
unfurl_media: false
}'
)"
response="$(
curl -fsS -X POST "${SLACK_WEBHOOK_URL}" \
-H "Content-Type: application/json" \
-d "${payload}"
)"
if [[ "${response}" != "ok" ]]; then
echo "::error::Slack webhook returned unexpected response: ${response}" >&2
exit 1
fi