Unified security code scanning system with CodeQL and Semgrep
This monorepo provides a reusable security scanning workflow with automatic language detection and parallel execution:
.github/workflows/security-scan.yml- Main reusable workflow (orchestrator).github/- Workspace package for workflow versioning (@metamask/action-code-scanner-github)packages/language-detector/- Detects languages and creates scan matrixpackages/codeql-action/- Custom CodeQL analysis with repo-specific configspackages/semgrep-action/- Semgrep pattern-based scanner
Add to your repository's .github/workflows/security.yml:
name: 'Security Scan'on: [push, pull_request]jobs:
security-scan:
uses: MetaMask/action-security-code-scanner/.github/workflows/security-scan.yml@v2with:
scanner-ref: v2permissions:
actions: readcontents: readsecurity-events: writeThe workflow will:
- Auto-detect languages in your repository
- Load repo-specific config from
repo-configs/(or use defaults) - Run CodeQL and Semgrep scans in parallel
- Upload SARIF results to GitHub Security tab
Option 1: File-based config (recommended)
Create repo-configs/<your-repo-name>.js in this monorepo:
constconfig={pathsIgnored: ['test','docs'],rulesExcluded: ['js/log-injection'],languages_config: [{language: 'java-kotlin',build_mode: 'manual',build_command: './gradlew build',version: '21',distribution: 'temurin',},],queries: [{name: 'Security queries',uses: './query-suites/base.qls'},{name: 'Custom queries',uses: './custom-queries/query-suites/custom-queries.qls',},],};exportdefaultconfig;Option 2: Workflow input (overrides file config)
jobs:
security-scan:
uses: metamask/security-codescanner-monorepo/.github/workflows/security-scan.yml@mainwith:
repo: ${{ github.repository }}languages_config: | [ { "language": "java-kotlin", "build_mode": "manual", "build_command": "./gradlew build", "version": "21" } ]paths_ignored: 'test,docs'rules_excluded: 'js/log-injection,py/sql-injection'When testing changes to the security scanner itself from a dev branch, you must explicitly pass the ref input:
jobs:
security-scan:
uses: metamask/security-codescanner-monorepo/.github/workflows/security-scan.yml@dev-branchwith:
repo: ${{ github.repository }}ref: dev-branch # Must explicitly pass the branch nameNote: The @branch in the uses: statement only affects which workflow file is used. The ref input ensures all internal monorepo checkouts use the same branch.
security-scanner-monorepo/
├── .github/ # Workflow workspace package
│ ├── package.json
│ └── workflows/
│ └── security-scan.yml # Main reusable workflow
├── packages/
│ ├── language-detector/ # Language detection & matrix creation
│ ├── codeql-action/ # CodeQL scanner
│ │ ├── repo-configs/ # Repository-specific configs
│ │ ├── query-suites/ # CodeQL query suites
│ │ ├── scripts/ # Config generation scripts
│ │ └── src/ # Shared utilities
│ └── semgrep-action/ # Semgrep scanner
└── SECURITY.md # Security model documentation
# Install dependencies
yarn install
# Run linting
yarn lint
# Fix formatting
yarn lint:fix# Test language detector
yarn workspace @metamask/language-detector test# Test with integration tests
yarn workspace @metamask/language-detector test:integration# Run command in specific package
yarn workspace @metamask/language-detector <command># Run command in all packages
yarn workspaces foreach run <command>{// Paths to ignore during scanpathsIgnored: ['test','vendor'],// Rule IDs to excluderulesExcluded: ['js/log-injection'],// Per-language configurationlanguages_config: [{language: 'java-kotlin',// CodeQL languageignore: false,// Skip this language (optional)build_mode: 'manual',// 'none', 'autobuild', or 'manual'build_command: './gradlew build',version: '21',// Language/runtime versiondistribution: 'temurin'// Distribution (Java/Node.js)}],// CodeQL query suitesqueries: [{name: 'Base queries',uses: './query-suites/base.qls'}]}CodeQL:
- JavaScript/TypeScript →
javascript-typescript - Python →
python - Java/Kotlin →
java-kotlin - Go →
go - C/C++ →
cpp - C# →
csharp - Ruby →
ruby
Semgrep: All languages (language-agnostic pattern matching)
- Detects languages via GitHub API
- Maps to appropriate scanners
- Configurable per-repository
- Parallel scanning per language
- Matrix-based job strategy
- Fail-fast for ignored languages
- File-based configs (single source of truth)
- Workflow input overrides
- Per-language build settings
- Minimal token permissions (
contents: read,security-events: write) - Input validation and sanitization
- See SECURITY.md for threat model
- Check GitHub's language detection (repo insights → languages)
- Ensure language is in
LANGUAGE_MAPPINGinlanguage-detector/src/job-configurator.js - Add manual
languages_configin workflow input
- Verify
build_commandin repo config - Check if correct
versionanddistributionare specified - Review CodeQL build logs in Actions
- Repo config filename must match repo name:
owner/repo→repo.js - Ensure config file exports with
export default config - Check config-loader logs in workflow output
- Add required permissions to calling workflow:
permissions: actions: readcontents: readsecurity-events: write
ISC
See SECURITY.md for security model and REVIEW_TRACKING.md for current development status.