Security Scanning

Reference for the security-scanning workflows scaffolded for the Stella Ops CI/CD pipeline. Audience: security and platform engineers reactivating, wiring, or auditing these scans.

Status — scaffolded but not active (verified 2026-05-30). Every workflow described on this page (sast-scan.yml, secrets-scan.yml, container-scan.yml, dependency-security-scan.yml, dependency-license-gate.yml, nightly-regression.yml) currently lives in .gitea/workflows-archived/, not in the active .gitea/workflows/ directory. Per .gitea/README.md, the bulk of the historical workflow catalogue was archived on 2026-02-01 while the local-CI scaffolding is validated. The scanning jobs are also placeholder scaffolds: the SAST, secrets, and container-scan jobs ship with the scanner step commented out and only emit a ::notice:: placeholder until an operator uncomments one of the documented tool options. The .NET security analyzer, dotnet list package --vulnerable, Hadolint, dotnet-delice, and license-checker steps are wired and runnable; the third-party SAST/secrets/container scanners are not. Treat this document as the design/restoration reference, not a description of jobs running on every push today. To reactivate, move a file back into .gitea/workflows/ (see .gitea/README.md § Restoring a workflow).


Security Scanning Overview

Stella Ops implements a defense-in-depth security scanning strategy:

┌─────────────────────────────────────────────────────────────────┐
│                    SECURITY SCANNING LAYERS                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Layer 1: PRE-COMMIT                                            │
│  └── Secrets scanning (pre-commit hook)                         │
│                                                                 │
│  Layer 2: PULL REQUEST                                          │
│  ├── SAST (Static Application Security Testing)                 │
│  ├── Secrets scanning                                           │
│  ├── Dependency vulnerability audit                             │
│  └── License compliance check                                   │
│                                                                 │
│  Layer 3: MAIN BRANCH                                           │
│  ├── All Layer 2 scans                                          │
│  ├── Container image scanning                                   │
│  └── Extended SAST analysis                                     │
│                                                                 │
│  Layer 4: SCHEDULED                                             │
│  ├── Weekly deep SAST scan (Monday)                             │
│  ├── Weekly dependency audit (Sunday)                           │
│  ├── Daily container scanning                                   │
│  └── Nightly regression security tests                          │
│                                                                 │
│  Layer 5: RELEASE                                               │
│  ├── Final vulnerability gate                                   │
│  ├── SBOM generation and signing                                │
│  ├── Provenance attestation                                     │
│  └── Container signing                                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

This layering is the intended defense-in-depth design. The scanning workflows that realise Layers 1–4 are currently archived placeholders (see the status banner above); the Layer 5 release-time SBOM/provenance/signing steps live in the release pipelines (release-suite.yml, service-release.yml, artifact-signing.yml, etc., also archived) and are documented separately in Release Pipelines. Per-workflow status is called out in each subsection below.


Scanning Workflows

1. SAST Scanning (sast-scan.yml)

Archived scaffold — .gitea/workflows-archived/sast-scan.yml. The cross-language SAST job (sast-scan) is a placeholder that prints tool options and runs no scanner until an operator uncomments one of Semgrep / CodeQL / SonarQube / Snyk Code. The dotnet-security, dependency-check, and dockerfile-lint jobs are fully wired.

Purpose: Detect security vulnerabilities in source code through static analysis.

Triggers (as authored in the archived workflow):

Path-filtered languages (push/PR trigger globs):

(The cross-language SAST options below cover YAML too once a scanner is wired; the trigger globs themselves do not list *.yml/*.yaml.)

Checks Performed:

CheckToolScopeWired?
Code vulnerabilitiesSemgrep / CodeQL / SonarQube / Snyk Code (placeholder)All sourceNo — placeholder, prints options only
.NET security analyzersBuilt-in Roslyn (EnableNETAnalyzers, /warnaserror on security CA rules)C# codeYes
Dependency vulnerabilitiesdotnet list package --vulnerable --include-transitiveNuGet packagesYes
Dockerfile best practicesHadolint v2.12.0DockerfilesYes

Configuration:

# sast-scan.yml inputs
workflow_dispatch:
  inputs:
    scan_level:
      type: choice
      options:
        - quick        # Fast scan, critical issues only
        - standard     # Default, balanced coverage
        - comprehensive # Full scan, all rules
    fail_on_findings:
      type: boolean
      default: true    # Block on findings

.NET Security Analyzer Rules:

The workflow enforces these security-critical CA rules as errors:

CategoryRulesDescription
SQL InjectionCA2100Review SQL queries for vulnerabilities
CryptographyCA5350-5403Weak crypto, insecure algorithms
DeserializationCA2300-2362Unsafe deserialization
XML SecurityCA3001-3012XXE, XPath injection
Web SecurityCA3061, CA5358-5398XSS, CSRF, CORS

2. Secrets Scanning (secrets-scan.yml)

Archived scaffold — .gitea/workflows-archived/secrets-scan.yml. The single secrets-scan job is a placeholder: the scanner action is commented out and the step prints the TruffleHog / Gitleaks / Semgrep options. No detection patterns are configured in the workflow itself — the table below is illustrative of what a wired scanner would catch.

Purpose: Detect hardcoded credentials, API keys, and secrets in code.

Triggers:

Detection Patterns (illustrative — not configured in the placeholder workflow):

Secret TypeExample Pattern
API Keyssk_live_[a-zA-Z0-9]+
AWS KeysAKIA[0-9A-Z]{16}
Private Keys-----BEGIN RSA PRIVATE KEY-----
Connection StringsPassword=.*;User ID=.*
JWT TokenseyJ[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+
GitHub Tokensgh[ps]_[A-Za-z0-9]{36}

Tool Options (Placeholder):

# Choose one by uncommenting in sast-scan.yml:

# Option 1: TruffleHog (recommended for open source)
# - name: TruffleHog Scan
#   uses: trufflesecurity/trufflehog@main
#   with:
#     extra_args: --only-verified

# Option 2: Gitleaks
# - name: Gitleaks Scan
#   uses: gitleaks/gitleaks-action@v2
#   env:
#     GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}

# Option 3: Semgrep
# - name: Semgrep Secrets
#   uses: returntocorp/semgrep-action@v1
#   with:
#     config: p/secrets

Allowlist Configuration:

Create .gitleaksignore or .secretsignore for false positives:

# Ignore test fixtures
src/__Tests/**/*
docs/modules/**/samples/**/*

# Ignore specific files
path/to/test-credentials.json

# Ignore by rule ID
[allowlist]
regexes = ["test_api_key_[a-z]+"]

3. Container Scanning (container-scan.yml)

Archived scaffold — .gitea/workflows-archived/container-scan.yml. The discover-images job dynamically finds every Dockerfile/Dockerfile.* in the tree, builds each as a throwaway scan-<name>:<sha> image, and (in the placeholder scan-images matrix step) prints the Trivy / Grype / Snyk options and writes a placeholder JSON report. No scanner runs and no fixed image catalogue is defined until an operator uncomments a scanner.

Purpose: Scan container images for OS and application vulnerabilities.

Triggers:

Scan Targets (discovered dynamically, not a fixed list):

The workflow does not scan a fixed set of published stellaops/* images. It enumerates Dockerfiles at scan time and builds a transient scan-<dir-name>:<sha> image per Dockerfile, deriving <dir-name> from the parent directory (or the devops/docker/... path). Each discovered image is scanned for OS packages and application-layer components by the chosen scanner once wired.

Vulnerability Severity Levels:

SeverityActionExample
CRITICALBlock releaseRemote code execution
HIGHBlock release (configurable)Privilege escalation
MEDIUMWarningInformation disclosure
LOWLog onlyMinor issues
UNKNOWNLog onlyUnclassified

Tool Options (Placeholder):

# Choose one by uncommenting in container-scan.yml:

# Option 1: Trivy (recommended)
# - name: Trivy Scan
#   uses: aquasecurity/trivy-action@master
#   with:
#     image-ref: ${{ steps.build.outputs.image }}
#     format: sarif
#     output: trivy-results.sarif
#     severity: CRITICAL,HIGH

# Option 2: Grype
# - name: Grype Scan
#   uses: anchore/scan-action@v3
#   with:
#     image: ${{ steps.build.outputs.image }}
#     fail-build: true
#     severity-cutoff: high

# Option 3: Snyk Container
# - name: Snyk Container
#   uses: snyk/actions/docker@master
#   env:
#     SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

4. Dependency Security Scanning (dependency-security-scan.yml)

Archived scaffold — .gitea/workflows-archived/dependency-security-scan.yml. Unlike the SAST/secrets/container placeholders, this workflow’s scanning steps are fully wired: scan-nuget runs dotnet list package --vulnerable, scan-npm runs npm audit --json, and the gate fails on critical findings. It is archived (inactive) only because the whole catalogue was archived, not because it is a stub.

Purpose: Audit NuGet and npm packages for known vulnerabilities.

Triggers:

Gate behaviour: the summary job fails only on critical NuGet+npm findings (TOTAL_CRITICAL > 0) when run on a pull request or when fail_on_vulnerabilities=true. High/medium/low findings are reported but do not block.

Scanned Files:

EcosystemFiles
NuGetsrc/Directory.Packages.props, **/*.csproj
npm**/package.json, **/package-lock.json

Vulnerability Sources:

Scan Process:

┌─────────────────────────────────────────────────────────────────┐
│                 DEPENDENCY SECURITY SCAN                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────────┐                                               │
│  │  scan-nuget  │ dotnet list package --vulnerable              │
│  └──────┬───────┘                                               │
│         │                                                       │
│         ▼                                                       │
│  ┌──────────────┐                                               │
│  │   scan-npm   │ npm audit --json                              │
│  └──────┬───────┘                                               │
│         │                                                       │
│         ▼                                                       │
│  ┌──────────────┐                                               │
│  │   summary    │ Aggregate results, generate report            │
│  └──────────────┘                                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Example Output:

## Dependency Vulnerability Audit

### NuGet Packages
| Package | Installed | Vulnerable | Severity | Advisory |
|---------|-----------|------------|----------|----------|
| Newtonsoft.Json | 12.0.1 | < 13.0.1 | HIGH | GHSA-xxxx |

### npm Packages
| Package | Installed | Vulnerable | Severity | Advisory |
|---------|-----------|------------|----------|----------|
| lodash | 4.17.15 | < 4.17.21 | CRITICAL | npm:lodash:1 |

5. License Compliance (dependency-license-gate.yml)

Archived scaffold — .gitea/workflows-archived/dependency-license-gate.yml. Triggers on pull requests touching src/Directory.Packages.props, **/*.csproj, **/package.json, or **/package-lock.json (no schedule, no push). NuGet licenses are checked with dotnet-delice; npm licenses with license-checker. The gate job fails the PR if any blocked license is detected.

Purpose: Ensure all dependencies use approved licenses.

The archived workflow encodes its allow/block lists as two env vars. The lists below are copied verbatim from the workflow — they do not match the BUSL-1.1 license model documented for the product elsewhere (see the flag below), so treat them as the workflow’s current state, not as policy guidance.

Allowed licenses (ALLOWED_LICENSES): MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, 0BSD, Unlicense, CC0-1.0, LGPL-2.1, LGPL-3.0, MPL-2.0, AGPL-3.0, GPL-3.0.

Blocked licenses (BLOCKED_LICENSES): GPL-2.0-only, SSPL-1.0, BUSL-1.1, Proprietary, Commercial.

Flag — stale/anomalous policy. The archived workflow’s gate message says the blocked set is “not compatible with AGPL-3.0” and lists BUSL-1.1 as a blocked license. Stella Ops itself ships under BUSL-1.1, and the repo-wide dependency gate (AGENTS.md § 2.6) requires BUSL-1.1 compatibility. The allow-list here also admits full GPL-3.0/AGPL-3.0 copyleft, which contradicts the “Blocked (copyleft)” intent the earlier version of this doc described. This workflow appears to predate the BUSL-1.1 licensing decision and is almost certainly not the authoritative dependency-license policy. The authoritative gate lives in NOTICE.md + docs/legal/THIRD-PARTY-DEPENDENCIES.md (per AGENTS.md § 2.6). Reconcile before reactivating.


Scan Results & Reporting

GitHub Step Summary

All security scans generate GitHub Step Summary reports:

## SAST Scan Summary

| Check | Status |
|-------|--------|
| SAST Analysis | ✅ Pass |
| .NET Security | ⚠️ 3 warnings |
| Dependency Check | ✅ Pass |
| Dockerfile Lint | ✅ Pass |

### .NET Security Warnings
- CA5350: Weak cryptographic algorithm (src/Crypto/Legacy.cs:42)
- CA2100: SQL injection risk (src/Data/Query.cs:78)

SARIF Integration

Scan results are uploaded in SARIF format for IDE integration:

- name: Upload SARIF
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: scan-results.sarif

Artifact Retention

ArtifactRetention
SARIF files30 days
Vulnerability reports90 days
License audit logs1 year

Security Gates

Design targets, not all enforced today. The thresholds below describe the intended gating model. In the archived workflows as authored, the only programmatic security gate is dependency-security-scan.yml, which fails on critical dependency findings, and dependency-license-gate.yml, which fails on blocked-license findings. SAST/secrets/ container “block” rows depend on a scanner being wired (currently placeholders) and on the workflows being moved back into .gitea/workflows/.

PR Merge Requirements

GateThresholdBlock Merge?
SAST Critical0Yes
SAST High0Configurable
Secrets Found0Yes
Vulnerable Dependencies (Critical)0Yes
Vulnerable Dependencies (High)5Warning
License Violations0Yes

Release Requirements

GateThresholdBlock Release?
Container Scan (Critical)0Yes
Container Scan (High)0Yes
SBOM GenerationSuccessYes
Signature VerificationValidYes

Remediation Workflows

Dependency Vulnerability Fix

  1. Renovate Auto-Fix:

    # renovate.json
    {
      "vulnerabilityAlerts": {
        "enabled": true,
        "labels": ["security"],
        "automerge": false
      }
    }
    
  2. Manual Override:

    # Update specific package
    dotnet add package Newtonsoft.Json --version 13.0.3
    
    # Audit and fix npm
    npm audit fix
    

False Positive Suppression

.NET Analyzer Suppression:

// Suppress specific instance
#pragma warning disable CA2100 // Review SQL queries for vulnerability
var query = $"SELECT * FROM {tableName}";
#pragma warning restore CA2100

// Or in .editorconfig
[*.cs]
dotnet_diagnostic.CA2100.severity = none  # NOT RECOMMENDED

Semgrep/SAST Suppression:

// nosemgrep: sql-injection
var query = $"SELECT * FROM {tableName}";

Container Scan Ignore:

# .trivyignore
CVE-2021-44228  # Log4j - not applicable (no Java)
CVE-2022-12345  # Accepted risk with mitigation

Configuration Files

Location

Existence check (2026-05-30): of the files below, only .editorconfig currently exists in the repository. .gitleaksignore, .trivyignore, .semgrepignore, and renovate.json are not present at the repo root (or anywhere in the tree). They are the conventional config locations a wired-up scanner would read — create them when you activate the corresponding scanner. Renovate exists only as the archived renovate.yml workflow, with no committed renovate.json config.

FilePurposeLocationPresent?
.gitleaksignoreSecrets scan allowlistRepository rootNo (create when wiring Gitleaks)
.trivyignoreContainer scan ignore listRepository rootNo (create when wiring Trivy)
.semgrepignoreSAST ignore patternsRepository rootNo (create when wiring Semgrep)
renovate.jsonDependency update configRepository rootNo (workflow renovate.yml archived)
.editorconfigAnalyzer severityRepository rootYes

Example .trivyignore

# Ignore by CVE ID
CVE-2021-44228

# Ignore by package
pkg:npm/lodash@4.17.15

# Ignore with expiration
CVE-2022-12345 exp:2025-06-01

Scheduled Scan Summary

Schedules as authored in the archived workflows. All four are currently inactive (archived) and will not fire until restored to .gitea/workflows/. nightly-regression.yml is a full-suite regression run whose Security entry is a .NET test category in its matrix (run-test-category.sh Security), not a dedicated security-scanning job.

DayTime (UTC)WorkflowCronFocus
Daily2:00 AMnightly-regression.yml0 2 * * *Regression suite (incl. Security test category)
Daily4:00 AMcontainer-scan.yml0 4 * * *Image vulnerabilities (placeholder scanner)
Sunday2:00 AMdependency-security-scan.yml0 2 * * 0Package audit (wired)
Monday3:30 AMsast-scan.yml30 3 * * 1Code analysis (.NET analyzers wired; cross-language SAST placeholder)

Monitoring & Alerts

Illustrative pattern. None of the archived security-scan workflows currently wire a Slack step; the snippet below shows how to add one. (nightly-regression.yml only mentions “Slack/Teams on failure” in a header comment.) Add the notification step and supply the referenced secret when activating a scanner.

Notification Channels

Configure notifications for security findings:

# In workflow
- name: Notify on Critical
  if: steps.scan.outputs.critical_count > 0
  run: |
    curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
      -d '{"text":"🚨 Critical security finding in '${{ github.repository }}'"}'

Dashboard Integration

Security scan results can be exported to:


The previous “Dependency Management” (../operations/dependency-management.md) and “SBOM Guide” (../sbom/guide.md) links pointed at files that do not exist in this docs tree; they have been replaced with verified targets above. Renovate/dependency-update guidance lives in .gitea/docs/dependency-management.md in the source repo.