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 matrix
- packages/codeql-action/- Custom CodeQL analysis with repo-specific configs
- packages/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: v2The 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.