diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 488c496c..00000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,54 +0,0 @@ ---- -name: Code - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - - -permissions: - read-all - -jobs: - analyze: - name: Analyze (${{ matrix.language }}) - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-24.04' }} - permissions: - security-events: write - packages: read - actions: read - contents: read - - strategy: - fail-fast: false - matrix: - include: - - language: python - build-mode: none - - language: javascript-typescript - build-mode: none - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Initialize CodeQL - uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 - with: - category: "/language:${{matrix.language}}" diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml deleted file mode 100644 index 44ef8c1b..00000000 --- a/.github/workflows/container.yml +++ /dev/null @@ -1,124 +0,0 @@ ---- -name: Container - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -env: - REGISTRY: docker.io - REPOSITORY: jackseceng - IMAGE_NAME: linkshort - -jobs: - build: - name: Build and Push - runs-on: ubuntu-24.04 - permissions: - contents: read - packages: write - id-token: write # Required for provenance - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - - name: Build and Push - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 - with: - context: . - push: true - tags: | - ${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ github.event_name == 'pull_request' && github.event.number || 'latest' }} - provenance: true - sbom: true - - scan: - needs: build - name: ${{ matrix.scanner.name }} Scan - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - scanner: - - name: Grype - id: grype - - name: Trivy - id: trivy - - name: Scout - id: scout - - permissions: - contents: read - security-events: write - pull-requests: write # Required for Docker Scout to write comments - - steps: - - name: 'Get image tag' - id: get_tag - run: | - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - echo "tag=${{ github.event.number }}" >> "$GITHUB_OUTPUT" - else - echo "tag=latest" >> "$GITHUB_OUTPUT" - fi - - - name: Login to Docker Hub - if: matrix.scanner.id == 'scout' - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Run Grype Scan - if: matrix.scanner.id == 'grype' - uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 - with: - image: ${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }} - fail-build: false - severity-cutoff: medium - only-fixed: true - output-format: sarif - output-file: 'results.sarif' - - - name: Run Trivy Scan - if: matrix.scanner.id == 'trivy' - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 - with: - image-ref: '${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }}' - scan-type: 'image' - ignore-unfixed: true - severity: 'CRITICAL,HIGH,MEDIUM' - format: 'sarif' - output: 'results.sarif' - - - name: Run Docker Scout Scan - if: matrix.scanner.id == 'scout' - uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1.20.4 - with: - command: cves - image: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }} - only-severities: critical,high,medium - only-fixed: true - sarif-file: 'results.sarif' - write-comment: true - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload SARIF results to GitHub Code Scanning - uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 - with: - sarif_file: 'results.sarif' \ No newline at end of file diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml index 5c283fab..0b7d9580 100644 --- a/.github/workflows/conventional-commits.yml +++ b/.github/workflows/conventional-commits.yml @@ -17,4 +17,4 @@ jobs: uses: webiny/action-conventional-commits@faccb24fc2550dd15c0390d944379d2d8ed9690e # v1.3.1 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - allowed-commit-types: "feat,fix,chore,docs,refactor,build,ci" + allowed-commit-types: "feat,fix,chore,docs,refactor,build,ci,style,perf,test" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ae03bf80..8913e7da 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,7 +7,6 @@ on: pull_request: branches: [ "main" ] - permissions: read-all diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 00000000..3a9a9e40 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,282 @@ +--- +name: Security + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +env: + REGISTRY: docker.io + REPOSITORY: jackseceng + IMAGE_NAME: linkshort + +jobs: + changes: + name: Detect Changes + runs-on: ubuntu-24.04 + permissions: + contents: read + pull-requests: read + outputs: + code: ${{ steps.filter.outputs.code }} + container: ${{ steps.filter.outputs.container }} + scans_ran: ${{ steps.filter.outputs.code == 'true' || steps.filter.outputs.container == 'true' }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: filter + with: + filters: | + code: + - 'app/**' + - 'requirements.txt' + - '.github/workflows/security.yml' + container: + - 'Dockerfile' + - 'docker-compose.yaml' + - '.dockerignore' + - 'requirements.txt' + - '.github/workflows/security.yml' + + semgrep: + needs: changes + name: Analyze (semgrep) + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read + env: + SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} + container: + image: semgrep/semgrep + if: github.actor != 'dependabot[bot]' && needs.changes.outputs.code == 'true' + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - run: semgrep ci --sarif --output semgrep.sarif + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + with: + sarif_file: semgrep.sarif + if: always() + + codeql: + needs: changes + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-24.04 + if: needs.changes.outputs.code == 'true' + permissions: + security-events: write + packages: read + actions: read + contents: read + strategy: + fail-fast: false + matrix: + include: + - language: python + build-mode: none + - language: javascript-typescript + build-mode: none + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Initialize CodeQL + uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + with: + category: "/language:${{matrix.language}}" + + container: + needs: changes + name: Build and Push + runs-on: ubuntu-24.04 + if: needs.changes.outputs.container == 'true' + permissions: + contents: read + packages: write + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Login to Docker Hub + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up QEMU + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + - name: Build and Push + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + with: + context: . + push: true + tags: | + ${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ github.event_name == 'pull_request' && github.event.number || 'latest' }} + provenance: true + sbom: true + + container-scan: + needs: [changes, container] + if: needs.changes.outputs.container == 'true' + name: ${{ matrix.scanner.name }} Scan + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + scanner: + - name: Grype + id: grype + - name: Trivy + id: trivy + - name: Scout + id: scout + permissions: + contents: read + security-events: write + pull-requests: write + steps: + - name: Get image tag + id: get_tag + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "tag=${{ github.event.number }}" >> "$GITHUB_OUTPUT" + else + echo "tag=latest" >> "$GITHUB_OUTPUT" + fi + - name: Login to Docker Hub + if: matrix.scanner.id == 'scout' + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Run Grype Scan + if: matrix.scanner.id == 'grype' + uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 + with: + image: ${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }} + fail-build: false + severity-cutoff: medium + only-fixed: true + output-format: sarif + output-file: results.sarif + - name: Run Trivy Scan + if: matrix.scanner.id == 'trivy' + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 + with: + image-ref: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }} + scan-type: image + ignore-unfixed: false + severity: CRITICAL,HIGH,MEDIUM + format: sarif + output: results.sarif + - name: Run Docker Scout Scan + if: matrix.scanner.id == 'scout' + uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1.20.4 + with: + command: cves + image: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }} + only-severities: critical,high,medium + only-fixed: true + sarif-file: results.sarif + write-comment: true + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Upload SARIF results + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + with: + sarif_file: results.sarif + + comment: + needs: [changes, semgrep, codeql, container-scan] + if: github.event_name == 'pull_request' && always() + runs-on: ubuntu-24.04 + permissions: + security-events: read + pull-requests: write + actions: read + checks: read + steps: + - name: Fetch findings and post comment + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR: ${{ github.event.pull_request.number }} + run: | + declare -A tool_counts + tools=("Grype" "Trivy" "Docker Scout" "Semgrep PRO" "CodeQL") + HEAD_SHA="${{ github.event.pull_request.head.sha }}" + + all_alerts="[]" + page=1 + while true; do + batch=$(gh api "repos/$REPO/code-scanning/alerts?pr=$PR&state=open&per_page=100&page=$page" 2>/dev/null || echo "[]") + count=$(echo "$batch" | jq 'length') + [[ "$count" -eq 0 ]] && break + all_alerts=$(echo "$all_alerts $batch" | jq -s 'add') + head_batch=$(echo "$batch" | jq --arg sha "$HEAD_SHA" '[.[] | select(.most_recent_instance.commit_sha == $sha)]') + for tool in "${tools[@]}"; do + n=$(echo "$head_batch" | jq --arg t "$tool" '[.[] | select(.tool.name | ascii_downcase | contains($t | ascii_downcase))] | length') + tool_counts[$tool]=$(( ${tool_counts[$tool]:-0} + n )) + done + [[ "$count" -lt 100 ]] && break + (( page++ )) || true + done + + total=0 + for i in "${!tools[@]}"; do + tool="${tools[$i]}" + c=${tool_counts[$tool]:-0} + total=$(( total + c )) + done + + details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path | ltrimstr("linkshort/") | gsub("//"; "/")):\(.most_recent_instance.location.start_line) | \(.rule.description) | \(.rule.security_severity_level // .rule.severity) | \(.tool.name) |"' | grep -v '^$' || true) + + severity_order=("critical" "high" "error" "medium" "warning" "low" "note") + highest="none" + while IFS= read -r sev; do + for s in "${severity_order[@]}"; do + if [[ "${sev,,}" == "$s" ]]; then + for ranked in "${severity_order[@]}"; do + [[ "$ranked" == "$highest" ]] && break + [[ "$ranked" == "$s" ]] && highest="$s" && break + done + break + fi + done + done < <(echo "$all_alerts" | jq -r '.[] | (.rule.security_severity_level // .rule.severity)') + + case "$highest" in + critical) alert_type="CAUTION" ;; + high|error) alert_type="WARNING" ;; + medium) alert_type="IMPORTANT" ;; + *) alert_type="NOTE" ;; + esac + + sep=$'\n' + header="## 🔒 Security Findings Summary" + summary="> [!${alert_type}]${sep}> **$total** findings, some of which are **$highest** severity." + findings_table="| File | Description | Severity | Tool |${sep}|------|-------------|----------|------|${sep}${details}" + dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" + footer="> Any findings must be fixed or justified, see [repository PR guidance](https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs) for details." + body="${header}${sep}${sep}${summary}${sep}${sep}${dropdown}${sep}${sep}${footer}" + + existing=$(gh api "repos/$REPO/issues/$PR/comments" \ + --jq '.[] | select(.body | startswith("## 🔒 Security Findings Summary")) | .id' | head -1 || true) + [[ -n "$existing" ]] && gh api -X DELETE "repos/$REPO/issues/comments/$existing" || true + + echo "$body" > /tmp/comment-body.md + gh pr comment "$PR" --repo "$REPO" --body-file /tmp/comment-body.md || true + + scans_ran="${{ needs.changes.outputs.scans_ran }}" + if [[ "$scans_ran" == "true" && "$total" -gt 0 ]]; then + echo "::error::$total open security finding(s) found. Any findings must be fixed or justified, see repository PR guidance for details https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs" + exit 1 + fi diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml deleted file mode 100644 index de6ca661..00000000 --- a/.github/workflows/semgrep.yml +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: Code - -on: - push: - branches: [ "main" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] - -jobs: - semgrep: - name: Analyze (semgrep) - runs-on: ubuntu-latest - permissions: - contents: read # for actions/checkout to fetch code - security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status - env: - SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} - container: - image: semgrep/semgrep - if: (github.actor != 'dependabot[bot]') - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - run: semgrep ci --sarif --output semgrep.sarif - - # Upload SARIF file generated in previous step - - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 - with: - sarif_file: semgrep.sarif - if: always()