Skip to main content

ADR-006: CI Security Gate Pipeline

Status: Accepted Date: 2026-02-18 Deciders: CODITECT Architecture Team Source Research: ClawGuard AI Agent Security Ecosystem Evaluation (2026-02-18)


Context

With the YAML pattern library (ADR-002) and risk scoring model (ADR-004) implemented, the security layer needs a mechanism to enforce these patterns in CI/CD pipelines. Without automated enforcement, patterns exist only as documentation — developers must remember to run scans manually, which defeats the purpose of a security gate.

Three enforcement points were considered:

Enforcement PointWhen It RunsScopeLatency
Pre-commit hookBefore git commitLocal developer machine<2s for staged files
GitHub Actions workflowOn push/PR to protected branchesCI server, all changed files10-30s
Pre-merge checkBefore merge to mainCI server, full diff10-30s

The ClawGuard ecosystem provides no CI integration — all three repositories (maxxie114, superglue-ai, JaydenBeard) operate only as runtime middleware or CLI tools. CODITECT requires a shift-left approach where insecure patterns are caught before code reaches the main branch.

Key design questions:

  1. Trigger strategy: Should the gate run on every push, only on PRs, or both?
  2. File resolution: How should changed files be identified across push vs. PR contexts?
  3. Output format: How should findings be surfaced to developers?
  4. Fail threshold: At what severity should the pipeline block merge?
  5. Scanner architecture: Standalone script or integrated into the existing PatternEngine?

Decision

Implement a GitHub Actions workflow (security-scan.yml) that invokes a standalone Python scanner (scripts/ci-security-scan.py) reusing the existing PatternEngine and aggregate_score() from the core library.

Trigger Strategy

on:
push:
branches: [main]
pull_request:
branches: [main]

The workflow triggers on both push and PR events to main. Push triggers catch direct commits (e.g., maintainer hotfixes). PR triggers catch the standard development workflow and provide inline annotations on the diff.

File Resolution

The scanner resolves files to scan using a two-stage fallback:

  1. Explicit --files argument — used when the caller knows which files changed (e.g., GitHub Actions passes the changed file list).
  2. Git diff resolution — if no --files provided:
    • First tries git diff --cached --name-only --diff-filter=ACM (pre-commit context: staged files).
    • Falls back to git diff origin/main...HEAD --name-only --diff-filter=ACM (CI/PR context: branch diff).

This dual approach allows the same scanner to serve both pre-commit hooks and CI pipelines without configuration changes.

Output Format

Findings are emitted as GitHub Actions workflow commands:

::error file=src/config.py,line=42::[SD-001] AWS Access Key detected (matched: 'AKIAIOSFODNN7EXAMPLE' in field 'content')
::warning file=src/utils.py,line=15::[DC-006] Sudo usage detected (matched: 'sudo' in field 'content')
  • critical and high severity findings emit ::error annotations.
  • medium and low severity findings emit ::warning annotations.
  • Annotations appear inline on the PR diff, directly on the offending line.

A summary table is printed to stdout (captured by job summary):

========================================================
CODITECT CI Security Scan — Summary
========================================================
Patterns loaded : 25
Files scanned : 3
Total findings : 2

Severity Count
------------ ------
critical 0
high 1
medium 1
low 0
========================================================

Fail Threshold

The scanner exits with code 1 (blocking) when any finding meets or exceeds --fail-on severity. Default: high.

python3 scripts/ci-security-scan.py --fail-on high
--fail-onBlocks On
criticalOnly critical findings
highCritical + high (default)
mediumCritical + high + medium
lowAny finding

This makes the gate strict by default (blocks on high+critical) while allowing per-repository tuning.

Scanner Architecture

The scanner is a standalone script that imports from the src.security package:

scripts/ci-security-scan.py
├── imports: PatternEngine, Severity (from src.security.patterns)
├── imports: aggregate_score (from src.security.scoring)
├── _get_changed_files_from_git() — file resolution
├── _scan_file() — per-file scanning via PatternEngine.match()
├── _emit_annotation() — GitHub Actions annotation format
├── _print_summary() — human-readable summary table
└── main() — CLI entry point with argparse

The scanner uses "Write" as the synthetic tool name when calling PatternEngine.match(). This means:

  • Patterns with applies_to: [Write] or applies_to: [Bash, Write] are evaluated against file content.
  • Patterns restricted to applies_to: [Bash] only are intentionally excluded — file content is not a shell command.

Workflow Permissions

permissions:
contents: read

Least-privilege: the workflow only needs read access to check out code and scan files. No write permissions are granted.


Consequences

Positive

  • Shift-left enforcement: Insecure patterns are caught at PR time, before code reaches main. Developers see inline annotations directly on offending lines.
  • Reuses existing engine: The scanner imports PatternEngine and aggregate_score — no pattern duplication, no separate detection logic to maintain.
  • Dual-context support: Same scanner works in pre-commit hooks (staged files) and CI (branch diff) without configuration changes.
  • Configurable threshold: Teams can tune --fail-on per-repository. Security-critical repos use medium, application repos use high.
  • Risk scoring integration: The aggregate_score() call provides an informational risk score alongside the pass/fail decision, giving developers context about overall security posture.
  • Non-blocking warnings: Medium/low findings surface as warnings (visible but non-blocking), encouraging incremental improvement without blocking development velocity.

Negative

  • File-level scanning only: The scanner reads file content as flat text. It does not parse ASTs, so patterns that require syntactic context (e.g., distinguishing a string literal from a variable name) may produce false positives. Mitigation: patterns are authored with this constraint in mind (ADR-002).
  • No incremental scanning: The scanner rescans entire changed files, not just changed lines. For large files with one-line changes, this means scanning unchanged content. Acceptable for the current 25-pattern library; may need optimization at 100+ patterns.
  • GitHub-specific annotations: The ::error/::warning format is GitHub Actions-specific. GitLab CI or other platforms would need adapter output. Mitigation: the summary table is platform-agnostic.

Neutral

  • The scanner does not implement --output for JSON file export. Scan results are available via stdout capture. JSON export can be added when artifact archival requirements are defined.

Alternatives Considered

Alternative A: GitHub Actions Marketplace Action

Approach: Publish a reusable GitHub Action (coditect-ai/security-scan-action) that encapsulates the scanner with Docker or composite steps.

Deferred (not rejected) because:

  • A marketplace action is the right distribution mechanism for external consumers. However, the scanner is currently used only within coditect-dev-agent-security. Packaging as a marketplace action adds maintenance overhead (Dockerfile, action.yml, versioning) that is premature.
  • When the scanner is adopted by multiple CODITECT repositories, packaging as a marketplace action becomes worthwhile. This is a Phase 2 concern.

Alternative B: Pre-commit Hook Only (No CI)

Approach: Rely solely on a local pre-commit hook (scripts/ci-security-scan.py invoked by .pre-commit-config.yaml).

Rejected because:

  • Pre-commit hooks are bypass-able (--no-verify). A determined actor can skip them.
  • Pre-commit hooks do not run in CI — commits pushed directly to main (e.g., hotfixes, bot commits) would bypass scanning.
  • Pre-commit hooks provide no PR-level visibility — other reviewers cannot see scan results.

Alternative C: Integrated GitHub App / Check Suite

Approach: Build a GitHub App that uses the Checks API to post detailed findings with annotations, code suggestions, and re-run capability.

Rejected because:

  • Requires a hosted service (webhook receiver, OAuth flow, GitHub App registration). The CI scanner achieves equivalent PR-level visibility with zero infrastructure.
  • The ::error/::warning annotations provide the same inline PR experience as Check Suite annotations for the current use case.
  • Appropriate for a commercial product offering; premature for internal tooling.

Implementation Notes

  • Workflow file: .github/workflows/security-scan.yml
  • Scanner script: scripts/ci-security-scan.py
  • Dependencies: Python 3.10+, PyYAML (for pattern loading)
  • Test coverage: 41 unit tests for PatternEngine (loading + matching), 6 for scoring, 9 for actions
  • Known gap: The workflow references --output scan-results.json for artifact upload, but the scanner does not yet implement --output. The artifact upload step will no-op until this is added.

References

  • ADR-001: Agent Security Layer Architecture (runtime interceptor design)
  • ADR-002: Security Pattern Library Format (YAML pattern definitions)
  • ADR-003: Fail-Open vs Fail-Closed Security Gate (gate behavior)
  • ADR-004: Risk Scoring Model (aggregate_score formula)
  • ADR-005: Malicious Supply Chain Detection (future pattern category)
  • GitHub Actions workflow commands: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions