Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 84 additions & 4 deletions .github/workflows/pr-security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,25 @@
description: 'Enable Docker Hub Health Score compliance checks (non-root user, CVEs, licenses)'
type: boolean
default: true
enable_codeql:
description: 'Enable CodeQL static analysis. Runs as a separate job in parallel with other security scans'
type: boolean
default: false
codeql_languages:
description: 'Comma-separated CodeQL languages to analyze (e.g., go, javascript-typescript, actions). Required when enable_codeql is true'
type: string
default: ''
codeql_queries:
description: 'CodeQL query suite to use (security-standard, security-extended, security-and-quality)'
type: string
default: 'security-extended'

permissions:
id-token: write # Required for OIDC authentication
contents: read # Required to checkout the repository
pull-requests: write # Allows commenting on PRs
security-events: write # Required for security scanning
actions: read # Required for CodeQL on private repositories

jobs:
prepare_matrix:
Expand Down Expand Up @@ -207,17 +220,84 @@
# with:
# sarif_file: 'trivy-vulnerability-scan-docker-${{ env.APP_NAME }}.sarif'

# ----------------- CodeQL Analysis -----------------
codeql_analysis:
name: CodeQL Analysis
needs: prepare_matrix
if: inputs.enable_codeql && inputs.codeql_languages != ''
runs-on: ${{ inputs.runner_type }}
permissions:
contents: read
security-events: write
pull-requests: write
actions: read
steps:
- name: Checkout Repository
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Extract changed directories from matrix
id: extract-dirs
shell: bash
env:
MATRIX: ${{ needs.prepare_matrix.outputs.matrix }}
BASE_REF: ${{ github.event.pull_request.base.ref || 'develop' }}
run: |
PATH_MODE=dirs
if [ "$MATRIX" != "[]" ] && [ -n "$MATRIX" ]; then
DIRS=$(printf '%s' "$MATRIX" | jq -r '.[].working_dir' 2>/dev/null | tr '\n' ',' | sed 's/,$//')
fi

# Fallback for single-app repos (working_dir is ".")
if [ -z "$DIRS" ] || [ "$DIRS" = "." ]; then
PATH_MODE=files
DIRS=$(git diff --name-only --diff-filter=ACMR "origin/$BASE_REF"...HEAD | head -200 | tr '\n' ',' | sed 's/,$//')
fi

echo "paths=$DIRS" >> "$GITHUB_OUTPUT"
echo "path-mode=$PATH_MODE" >> "$GITHUB_OUTPUT"

- name: Generate CodeQL config for changed paths
id: codeql-config
uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-config@v1.18.0
Comment thread Fixed
with:
changed-paths: ${{ steps.extract-dirs.outputs.paths }}
path-mode: ${{ steps.extract-dirs.outputs.path-mode }}

- name: Initialize CodeQL
if: steps.codeql-config.outputs.skip != 'true'
uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-init@v1.18.0
Comment thread Fixed
with:
languages: ${{ inputs.codeql_languages }}
queries: ${{ inputs.codeql_queries }}
config-file: ${{ steps.codeql-config.outputs.config-file }}

- name: Perform CodeQL Analysis
if: steps.codeql-config.outputs.skip != 'true'
uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-analyze@v1.18.0
Comment thread Fixed
with:
category: '/language:${{ inputs.codeql_languages }}'

- name: Post CodeQL Results to PR
if: always() && github.event_name == 'pull_request' && steps.codeql-config.outputs.skip != 'true'
uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-reporter@v1.18.0
Comment thread Fixed
with:
github-token: ${{ secrets.MANAGE_TOKEN || secrets.GITHUB_TOKEN }}
languages: ${{ inputs.codeql_languages }}
fail-on-findings: 'false'
Comment thread
bedatty marked this conversation as resolved.
Outdated

# ----------------- Slack Notification -----------------
notify:
name: Notify
needs: [prepare_matrix, security_scan]
if: always() && needs.prepare_matrix.outputs.matrix != '[]'
needs: [prepare_matrix, security_scan, codeql_analysis]
if: always() && (needs.prepare_matrix.outputs.matrix != '[]' || needs.codeql_analysis.result != 'skipped')
runs-on: ${{ inputs.runner_type }}
steps:
- name: Slack Notification
uses: LerianStudio/github-actions-shared-workflows/src/notify/slack-notify@v1.18.0
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
status: ${{ needs.security_scan.result }}
status: ${{ (needs.security_scan.result == 'failure' || needs.codeql_analysis.result == 'failure') && 'failure' || 'success' }}
workflow-name: "PR Security Scan"
failed-jobs: ${{ needs.security_scan.result == 'failure' && 'Security Scan' || '' }}
failed-jobs: ${{ needs.security_scan.result == 'failure' && 'Security Scan' || '' }}${{ needs.codeql_analysis.result == 'failure' && ', CodeQL Analysis' || '' }}
41 changes: 41 additions & 0 deletions .github/workflows/self-pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,47 @@ jobs:
with:
files: ${{ needs.changed-files.outputs.composite_files }}

# ----------------- CodeQL Analysis -----------------
codeql:
name: CodeQL Analysis
runs-on: blacksmith-4vcpu-ubuntu-2404
needs: changed-files
if: needs.changed-files.outputs.action_files != ''
permissions:
contents: read
security-events: write
pull-requests: write
actions: read
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Generate CodeQL config for changed files
id: codeql-config
uses: ./src/security/codeql-config
with:
changed-paths: ${{ needs.changed-files.outputs.action_files }}

- name: Initialize CodeQL
if: steps.codeql-config.outputs.skip != 'true'
uses: ./src/security/codeql-init
with:
languages: actions
config-file: ${{ steps.codeql-config.outputs.config-file }}

- name: Perform CodeQL Analysis
if: steps.codeql-config.outputs.skip != 'true'
uses: ./src/security/codeql-analyze
with:
category: '/language:actions'

- name: Post CodeQL Results to PR
if: always() && github.event_name == 'pull_request' && steps.codeql-config.outputs.skip != 'true'
uses: ./src/security/codeql-reporter
with:
github-token: ${{ secrets.MANAGE_TOKEN || github.token }}
languages: actions

# ----------------- Lint Report -----------------
lint-report:
name: Lint Report
Expand Down
43 changes: 43 additions & 0 deletions src/security/codeql-analyze/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td><img src="https://github.com/LerianStudio.png" width="72" alt="Lerian" /></td>
<td><h1>codeql-analyze</h1></td>
</tr>
</table>

Composite action that wraps [`github/codeql-action/analyze`](https://github.com/github/codeql-action) to perform CodeQL analysis and upload SARIF results to the GitHub Security tab. Designed to be paired with [`codeql-init`](../codeql-init/).

## Inputs

| Input | Description | Required | Default |
|---|---|:---:|---|
| `category` | Category for SARIF results used for deduplication (e.g., `/language:actions`) | Yes | — |
| `output` | Output directory for SARIF files | No | `../results` |

## Usage

### Basic (paired with codeql-init)

```yaml
steps:
- uses: actions/checkout@v6

- name: Initialize CodeQL
uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-init@v1.x.x
with:
languages: actions

- name: Perform CodeQL Analysis
uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-analyze@v1.x.x
with:
category: '/language:actions'
```

## Permissions required

```yaml
permissions:
contents: read
security-events: write # required for SARIF upload to GitHub Security tab
actions: read # required for workflows using private repositories
```
Comment thread
bedatty marked this conversation as resolved.
20 changes: 20 additions & 0 deletions src/security/codeql-analyze/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: CodeQL Analyze
description: Perform CodeQL analysis and upload SARIF results to GitHub Security tab

inputs:
category:
description: 'Category for SARIF results used for deduplication (e.g., /language:actions)'
required: true
output:
description: 'Output directory for SARIF files'
required: false
default: '../results'

runs:
using: composite
steps:
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: ${{ inputs.category }}
output: ${{ inputs.output }}
57 changes: 57 additions & 0 deletions src/security/codeql-config/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td><img src="https://github.com/LerianStudio.png" width="72" alt="Lerian" /></td>
<td><h1>codeql-config</h1></td>
</tr>
</table>

Composite action that generates a dynamic CodeQL configuration file scoped to changed paths. Ensures CodeQL analyzes only modified files in PRs instead of the entire repository. Designed to run before [`codeql-init`](../codeql-init/).

## Inputs

| Input | Description | Required | Default |
|---|---|:---:|---|
| `changed-paths` | Comma or newline-separated list of changed file paths or directories | Yes | — |
| `output-file` | Path where the generated config file will be written | No | `.github/codeql-config-pr.yml` |
| `path-mode` | How to interpret paths: `files` extracts parent directories, `dirs` uses paths as-is | No | `files` |

## Outputs

| Output | Description |
|---|---|
| `config-file` | Path to the generated CodeQL config file |
| `skip` | `true` if no paths were resolved and CodeQL should be skipped |

## Usage

### With changed-workflows (this repo)

```yaml
- name: Generate CodeQL config
id: codeql-config
uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-config@v1.x.x
Comment thread
bedatty marked this conversation as resolved.
with:
changed-paths: ${{ needs.changed-files.outputs.action_files }}

- name: Initialize CodeQL
if: steps.codeql-config.outputs.skip != 'true'
uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-init@v1.x.x
with:
languages: actions
config-file: ${{ steps.codeql-config.outputs.config-file }}
```

### With monorepo matrix directories

```yaml
- name: Generate CodeQL config
id: codeql-config
uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-config@v1.x.x
with:
changed-paths: ${{ steps.extract-dirs.outputs.dirs }}
path-mode: dirs
```

## Permissions required

No special permissions needed — this composite only generates a file.
75 changes: 75 additions & 0 deletions src/security/codeql-config/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: CodeQL Config Generator
description: Generates a dynamic CodeQL configuration file scoped to changed paths for PR-only analysis.

inputs:
changed-paths:
description: 'Comma or newline-separated list of changed file paths or directories to scope the analysis'
required: true
output-file:
description: 'Path where the generated CodeQL config file will be written'
required: false
default: '.github/codeql-config-pr.yml'
path-mode:
description: 'How to interpret changed-paths: "files" extracts parent directories, "dirs" uses paths as-is'
required: false
default: 'files'

outputs:
config-file:
description: 'Path to the generated CodeQL config file'
value: ${{ steps.generate.outputs.config-file }}
skip:
description: 'true if no paths were resolved and CodeQL should be skipped'
value: ${{ steps.generate.outputs.skip }}
Comment thread
bedatty marked this conversation as resolved.

runs:
using: composite
steps:
- name: Generate CodeQL config
id: generate
shell: bash
env:
CONFIG_FILE: ${{ inputs.output-file }}
PATH_MODE: ${{ inputs.path-mode }}
RAW_PATHS: ${{ inputs.changed-paths }}
run: |
# Normalize separators (comma or newline) to newline
PATHS=$(printf '%s' "$RAW_PATHS" | tr ',' '\n' | sed '/^[[:space:]]*$/d' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')

if [ -z "$PATHS" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "No paths provided — skipping CodeQL config generation"
exit 0
fi
Comment thread
bedatty marked this conversation as resolved.

# Extract parent directories if path-mode is "files"
if [ "$PATH_MODE" = "files" ]; then
PATHS=$(echo "$PATHS" | while IFS= read -r file; do
dirname "$file"
done | sort -u)
fi

# Remove duplicates and empty lines
PATHS=$(echo "$PATHS" | sort -u | sed '/^[[:space:]]*$/d')

if [ -z "$PATHS" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "No paths resolved — skipping CodeQL config generation"
exit 0
fi

# Generate config
{
echo "paths:"
echo "$PATHS" | while IFS= read -r p; do
echo " - '$p'"
done
} > "$CONFIG_FILE"
Comment thread
bedatty marked this conversation as resolved.

echo "skip=false" >> "$GITHUB_OUTPUT"
echo "config-file=$CONFIG_FILE" >> "$GITHUB_OUTPUT"

count=$(echo "$PATHS" | wc -l | tr -d ' ')
echo "::group::CodeQL config — $count path(s)"
cat "$CONFIG_FILE"
echo "::endgroup::"
Loading