chore(security-scan): First version of the security scan workflow #4
Workflow file for this run
This file contains hidden or 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: Skill Security Scan | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: 'PR number to review' | |
| required: true | |
| type: number | |
| scan_all: | |
| description: 'Scan all packs (true) or changed only (false)' | |
| required: false | |
| default: 'false' | |
| type: choice | |
| options: | |
| - 'true' | |
| - 'false' | |
| jobs: | |
| security-scan: | |
| if: github.event.pull_request.draft == false || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install skill-scanner | |
| run: pip install 'cisco-ai-skill-scanner[google]' | |
| - name: Make scripts executable | |
| run: chmod +x scripts/detect-changed-packs.sh | |
| - name: Detect changed packs | |
| id: detect | |
| env: | |
| GITHUB_EVENT_NAME: ${{ github.event_name }} | |
| GITHUB_BASE_REF: ${{ github.base_ref }} | |
| GITHUB_EVENT_BEFORE: ${{ github.event.before }} | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ github.event.inputs.scan_all }}" = "true" ]; then | |
| PACKS=$(find . -maxdepth 2 -name skills -type d | sed 's|^\./||;s|/skills$||' | sort) | |
| else | |
| PACKS=$(./scripts/detect-changed-packs.sh || true) | |
| fi | |
| if [ -z "$PACKS" ]; then | |
| echo "changed=false" >> $GITHUB_OUTPUT | |
| echo "packs=" >> $GITHUB_OUTPUT | |
| echo "No packs with changes detected — skipping security scan" | |
| else | |
| echo "changed=true" >> $GITHUB_OUTPUT | |
| echo "packs<<EOF" >> $GITHUB_OUTPUT | |
| echo "$PACKS" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| echo "Changed packs detected:" | |
| echo "$PACKS" | |
| fi | |
| - name: Run security scan | |
| if: steps.detect.outputs.changed == 'true' | |
| id: scan | |
| env: | |
| SKILL_SCANNER_LLM_API_KEY: ${{ secrets.SKILL_SCANNER_LLM_API_KEY }} | |
| SKILL_SCANNER_LLM_MODEL: ${{ secrets.SKILL_SCANNER_LLM_MODEL }} | |
| run: | | |
| mkdir -p security-reports | |
| SCAN_FAILED=false | |
| while IFS= read -r pack; do | |
| [ -z "$pack" ] && continue | |
| echo "=== Scanning: $pack ===" | |
| skill-scanner scan-all "$pack" \ | |
| --recursive \ | |
| --lenient \ | |
| --use-behavioral \ | |
| --use-trigger \ | |
| --use-llm \ | |
| --llm-consensus-runs 3 \ | |
| --enable-meta \ | |
| --policy strict \ | |
| --fail-on-severity medium \ | |
| --format markdown \ | |
| --detailed \ | |
| --verbose \ | |
| --output "security-reports/security-report-${pack}.md" || SCAN_FAILED=true | |
| echo "" | |
| done <<< "${{ steps.detect.outputs.packs }}" | |
| if [ "$SCAN_FAILED" = "true" ]; then | |
| echo "scan_result=failed" >> $GITHUB_OUTPUT | |
| else | |
| echo "scan_result=passed" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Upload security reports | |
| if: steps.detect.outputs.changed == 'true' && always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: security-reports | |
| path: security-reports/ | |
| retention-days: 30 | |
| - name: Post scan summary | |
| if: steps.detect.outputs.changed == 'true' && github.event_name == 'pull_request' && always() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const glob = require('glob'); | |
| const reports = fs.readdirSync('security-reports').filter(f => f.endsWith('.md')); | |
| const scanResult = '${{ steps.scan.outputs.scan_result }}'; | |
| const icon = scanResult === 'passed' ? '✅' : '❌'; | |
| let body = `## ${icon} Skill Security Scan\n\n`; | |
| if (reports.length === 0) { | |
| body += 'No reports generated.\n'; | |
| } else { | |
| for (const report of reports) { | |
| const pack = report.replace('security-report-', '').replace('.md', ''); | |
| const content = fs.readFileSync(`security-reports/${report}`, 'utf8'); | |
| body += `<details>\n<summary>📋 ${pack}</summary>\n\n${content}\n\n</details>\n\n`; | |
| } | |
| } | |
| body += `\n\n> Reports are also available as [workflow artifacts](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const existing = comments.find(c => | |
| c.user.type === 'Bot' && c.body.includes('Skill Security Scan') | |
| ); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); | |
| } | |
| - name: Check scan results | |
| if: steps.detect.outputs.changed == 'true' | |
| run: | | |
| if [ "${{ steps.scan.outputs.scan_result }}" = "failed" ]; then | |
| echo "❌ Security scan found MEDIUM or higher severity issues — blocking merge" | |
| exit 1 | |
| else | |
| echo "✅ Security scan passed — no MEDIUM or higher severity issues found" | |
| fi |