Skip to content

ADR 0001 — Single CI on Azure DevOps and security gates (SAST/SCA/secret/DAST) ​

  • Status: Accepted; the platform decision is superseded by ADR 0008
  • Date: 2026-06-23
  • Context: Epic #59 (Continuous security tooling), User Story #60 (F-19)

Analyzer, dependency and post-merge enforcement details were strengthened by ADR 0009. ADR 0009 takes precedence where the policies differ.

Context ​

The repository previously ran a redundant GitHub Actions workflow (.github/workflows/ci.yml) in parallel with the Azure DevOps pipeline (azure-pipelines.yml). The Azure pipeline already covered everything the GitHub workflow did (commit linting, build, test, coverage, artifact/tenancy guards) and more (Stryker mutation testing, NuGet pack/publish, docs deploy). There were no real security gates: the only "secret" check was an ad-hoc git grep over templates/ and docs/.

The platform decision (June 2026) is a single CI on Azure DevOps. GitHub Actions is removed entirely, so GitHub-native features (CodeQL, Dependabot, GitHub secret scanning) are out of scope. All gates must run inside azure-pipelines.yml using cross-platform tooling.

Decision ​

1. Remove GitHub Actions ​

.github/workflows/ci.yml and the .github folder are deleted. The Azure pipeline is the single source of truth for CI. Codecov (which used the GitHub-only CODECOV_TOKEN) is dropped — coverage is already published to Azure DevOps via PublishCodeCoverageResults@2.

2. SAST — Roslyn analyzers + SonarCloud ​

  • Directory.Build.props enables EnableNETAnalyzers, AnalysisLevel=latest and AnalysisModeSecurity=All so the full Roslyn security category runs.
  • The original decision elevated only a curated set of high-confidence vulnerability rules. ADR 0009 supersedes that enforcement detail: Directory.Build.props now enables TreatWarningsAsErrors for Release, so a plain local Release build and the CI build enforce the same zero-warning contract.
  • SecurityCodeScan was rejected: it is not maintained for .NET 10. The built-in Roslyn security analyzers cover the same rule families and ship with the SDK.
  • SonarCloud (vs SonarQube vs GitHub Advanced Security for Azure DevOps) is the chosen quality gate. Rationale: SaaS (no infrastructure to run), native Azure DevOps extension, free for the current usage. It wraps the build in BuildAndTest (SonarCloudPrepare/Analyze/Publish), gated by the SONAR_ENABLED variable plus a SonarCloud service connection and the SONAR_ORG / SONAR_PROJECT_KEY variables.

Triaged analyzer exceptions ​

Two security rules are kept at warning (surfaced by Sonar) with justified per-site [SuppressMessage] attributes, after security review:

  • CA5350 (weak crypto / HMAC-SHA1): mandated by RFC 6238/4226 for TOTP interoperability (TotpService). SHA-256/512 are also supported.
  • CA5394 (insecure randomness): only used for cache-stampede jitter, retry backoff jitter, list shuffles, and deterministic test data — never for tokens, keys, or security decisions.

3. SCA — dotnet list package --vulnerable + Renovate ​

  • scripts/check-vulnerable-packages.sh runs dotnet list package --vulnerable --include-transitive, parses the output and fails explicitly (the CLI returns exit 0 even when vulnerabilities exist). Output is forced to English (DOTNET_CLI_UI_LANGUAGE) for locale-independent parsing, with an advisory-URL fallback.
  • Pre-existing transitive vulnerabilities were remediated to make the gate green: MailKit 4.17.0, Scriban 7.2.5, OpenTelemetry 1.16.0, SharpCompress 0.49.1, and SQLitePCLRaw pinned to the 3.x line (bundle_e_sqlite3 3.0.0). The MaxMind tar.gz extraction was migrated off SharpCompress to the .NET BCL (System.Formats.Tar + GZipStream).
  • Renovate (renovate.json) is chosen over Dependabot (GitHub-only) because it works natively against Azure Repos and opens dependency-update PRs to develop.

4. Secret scanning — gitleaks ​

  • scripts/run-gitleaks.sh runs a version-pinned gitleaks over the immutable commit range introduced by each CI event; .gitleaks.toml is the single ruleset (extends the upstream default plus the legacy placeholder patterns). Full-history rescans are separate maintenance audits, not work repeated by every PR or merge.
  • A Husky pre-commit hook runs gitleaks on staged changes for fast local feedback (warns, does not hard-block, when gitleaks is not installed).
  • The ad-hoc "secret placeholder guard" pipeline step is removed (consolidated into gitleaks — DRY).

5. DAST — OWASP ZAP baseline (nightly) ​

  • A scheduled Dast stage runs the pinned ghcr.io/zaproxy/zaproxy:2.15.0 baseline scan against a staging deployment (ZAP_TARGET_URL) and publishes the HTML/JSON report as a pipeline artifact. It never blocks PRs.

6. Mutation testing ​

Already implemented (Stryker MutationTesting stage + nightly). Not reimplemented here; threshold/scope are owned by US-114.

7. Branch policy (manual, Azure Repos) ​

The pipeline exposes the gates; enforcement as required PR checks is configured in the Azure DevOps portal (cannot be set from the repo):

  • Project Settings → Repositories → Gryd.IO → Policies → develop → Build Validation: require the Gryd.IO pipeline (covers SecurityGates + BuildAndTest).
  • Create a SonarCloud service connection and set SONAR_ENABLED=true, SONAR_ORG, SONAR_PROJECT_KEY.
  • Set ZAP_TARGET_URL to the staging URL to activate nightly DAST.
  • Install the Renovate app/pipeline for Azure Repos.

Consequences ​

  • One CI system to maintain; no drift between GitHub and Azure.
  • Security gates (SAST analyzers, SCA, secret scanning) fail PRs automatically; DAST and mutation testing run nightly.
  • Gate logic lives in reusable scripts/ (DRY) with pinned tool versions.
  • Some gates (Sonar, DAST, Renovate, branch policy) require one-time portal/ service-connection setup before they are fully active; the pipeline stays green until then thanks to variable gating.

Updated at:

Released under the MIT License.