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)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@v2
    with:
      scanner-ref: v2
    permissions:
      actions: read
      contents: read
      security-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:
const config = {
  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',
    },
  ],
};
export default config;Option 2: Workflow input (overrides file config)
jobs:
  security-scan:
    uses: metamask/security-codescanner-monorepo/.github/workflows/security-scan.yml@main
    with:
      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-branch
    with:
      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/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 scan
  pathsIgnored: ['test', 'vendor'],
  // Rule IDs to exclude
  rulesExcluded: ['js/log-injection'],
  // Per-language configuration
  languages_config: [
    {
      language: 'java-kotlin',      // CodeQL language
      ignore: false,                 // Skip this language (optional)
      build_mode: 'manual',          // 'none', 'autobuild', or 'manual'
      build_command: './gradlew build',
      version: '21',                 // Language/runtime version
      distribution: 'temurin'        // Distribution (Java/Node.js)
    }
  ],
  // CodeQL query suites
  queries: [
    { 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: read contents: read security-events: write
 
ISC
See SECURITY.md for security model and REVIEW_TRACKING.md for current development status.