Appearance
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.propsenablesEnableNETAnalyzers,AnalysisLevel=latestandAnalysisModeSecurity=Allso 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.propsnow enablesTreatWarningsAsErrorsfor 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 theSONAR_ENABLEDvariable plus aSonarCloudservice connection and theSONAR_ORG/SONAR_PROJECT_KEYvariables.
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.shrunsdotnet 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_sqlite33.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 todevelop.
4. Secret scanning — gitleaks
scripts/run-gitleaks.shruns a version-pinned gitleaks over the immutable commit range introduced by each CI event;.gitleaks.tomlis 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-commithook 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
Daststage runs the pinnedghcr.io/zaproxy/zaproxy:2.15.0baseline 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 theGryd.IOpipeline (covers SecurityGates + BuildAndTest). - Create a SonarCloud service connection and set
SONAR_ENABLED=true,SONAR_ORG,SONAR_PROJECT_KEY. - Set
ZAP_TARGET_URLto 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.