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, andlicense-checkersteps 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. Thedotnet-security,dependency-check, anddockerfile-lintjobs are fully wired.
Purpose: Detect security vulnerabilities in source code through static analysis.
Triggers (as authored in the archived workflow):
- Pull requests touching
src/**,*.csproj,*.cs,*.ts,*.js,*.py,Dockerfile* - Push to
main/developon the same paths - Weekly Monday 3:30 AM UTC (
cron: '30 3 * * 1') - Manual dispatch (
scan_level,fail_on_findingsinputs)
Path-filtered languages (push/PR trigger globs):
- C# / .NET (
*.cs,*.csproj) - JavaScript / TypeScript (
*.js,*.ts) - Python (
*.py) - Dockerfile (
Dockerfile*)
(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:
| Check | Tool | Scope | Wired? |
|---|---|---|---|
| Code vulnerabilities | Semgrep / CodeQL / SonarQube / Snyk Code (placeholder) | All source | No — placeholder, prints options only |
| .NET security analyzers | Built-in Roslyn (EnableNETAnalyzers, /warnaserror on security CA rules) | C# code | Yes |
| Dependency vulnerabilities | dotnet list package --vulnerable --include-transitive | NuGet packages | Yes |
| Dockerfile best practices | Hadolint v2.12.0 | Dockerfiles | Yes |
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:
| Category | Rules | Description |
|---|---|---|
| SQL Injection | CA2100 | Review SQL queries for vulnerabilities |
| Cryptography | CA5350-5403 | Weak crypto, insecure algorithms |
| Deserialization | CA2300-2362 | Unsafe deserialization |
| XML Security | CA3001-3012 | XXE, XPath injection |
| Web Security | CA3061, CA5358-5398 | XSS, CSRF, CORS |
2. Secrets Scanning (secrets-scan.yml)
Archived scaffold —
.gitea/workflows-archived/secrets-scan.yml. The singlesecrets-scanjob 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:
- Pull requests (all paths)
- Push to
main/develop - Manual dispatch (
scan_historyboolean input; whentrue, checkout uses full historyfetch-depth: 0instead of the default shallowfetch-depth: 50)
Detection Patterns (illustrative — not configured in the placeholder workflow):
| Secret Type | Example Pattern |
|---|---|
| API Keys | sk_live_[a-zA-Z0-9]+ |
| AWS Keys | AKIA[0-9A-Z]{16} |
| Private Keys | -----BEGIN RSA PRIVATE KEY----- |
| Connection Strings | Password=.*;User ID=.* |
| JWT Tokens | eyJ[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+ |
| GitHub Tokens | gh[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. Thediscover-imagesjob dynamically finds everyDockerfile/Dockerfile.*in the tree, builds each as a throwawayscan-<name>:<sha>image, and (in the placeholderscan-imagesmatrix 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:
- Push/PR touching
**/Dockerfile,**/Dockerfile.*, ordevops/docker/** - Daily schedule (4 AM UTC,
cron: '0 4 * * *') - Manual dispatch (
severity_thresholdchoice, defaultHIGH; optionalimagestring)
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:
| Severity | Action | Example |
|---|---|---|
| CRITICAL | Block release | Remote code execution |
| HIGH | Block release (configurable) | Privilege escalation |
| MEDIUM | Warning | Information disclosure |
| LOW | Log only | Minor issues |
| UNKNOWN | Log only | Unclassified |
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-nugetrunsdotnet list package --vulnerable,scan-npmrunsnpm 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:
- Weekly Sunday 2 AM UTC (
cron: '0 2 * * 0') - Pull requests touching
src/Directory.Packages.props,**/*.csproj,**/package.json, or**/package-lock.json - Manual dispatch (
fail_on_vulnerabilitiesboolean, defaulttrue)
Gate behaviour: the
summaryjob fails only on critical NuGet+npm findings (TOTAL_CRITICAL > 0) when run on a pull request or whenfail_on_vulnerabilities=true. High/medium/low findings are reported but do not block.
Scanned Files:
| Ecosystem | Files |
|---|---|
| NuGet | src/Directory.Packages.props, **/*.csproj |
| npm | **/package.json, **/package-lock.json |
Vulnerability Sources:
- GitHub Advisory Database
- NVD (National Vulnerability Database)
- OSV (Open Source Vulnerabilities)
- Vendor security advisories
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 touchingsrc/Directory.Packages.props,**/*.csproj,**/package.json, or**/package-lock.json(no schedule, no push). NuGet licenses are checked withdotnet-delice; npm licenses withlicense-checker. Thegatejob 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
| Artifact | Retention |
|---|---|
| SARIF files | 30 days |
| Vulnerability reports | 90 days |
| License audit logs | 1 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, anddependency-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
| Gate | Threshold | Block Merge? |
|---|---|---|
| SAST Critical | 0 | Yes |
| SAST High | 0 | Configurable |
| Secrets Found | 0 | Yes |
| Vulnerable Dependencies (Critical) | 0 | Yes |
| Vulnerable Dependencies (High) | 5 | Warning |
| License Violations | 0 | Yes |
Release Requirements
| Gate | Threshold | Block Release? |
|---|---|---|
| Container Scan (Critical) | 0 | Yes |
| Container Scan (High) | 0 | Yes |
| SBOM Generation | Success | Yes |
| Signature Verification | Valid | Yes |
Remediation Workflows
Dependency Vulnerability Fix
Renovate Auto-Fix:
# renovate.json { "vulnerabilityAlerts": { "enabled": true, "labels": ["security"], "automerge": false } }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
.editorconfigcurrently exists in the repository..gitleaksignore,.trivyignore,.semgrepignore, andrenovate.jsonare 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 archivedrenovate.ymlworkflow, with no committedrenovate.jsonconfig.
| File | Purpose | Location | Present? |
|---|---|---|---|
.gitleaksignore | Secrets scan allowlist | Repository root | No (create when wiring Gitleaks) |
.trivyignore | Container scan ignore list | Repository root | No (create when wiring Trivy) |
.semgrepignore | SAST ignore patterns | Repository root | No (create when wiring Semgrep) |
renovate.json | Dependency update config | Repository root | No (workflow renovate.yml archived) |
.editorconfig | Analyzer severity | Repository root | Yes |
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.ymlis a full-suite regression run whoseSecurityentry is a .NET test category in its matrix (run-test-category.sh Security), not a dedicated security-scanning job.
| Day | Time (UTC) | Workflow | Cron | Focus |
|---|---|---|---|---|
| Daily | 2:00 AM | nightly-regression.yml | 0 2 * * * | Regression suite (incl. Security test category) |
| Daily | 4:00 AM | container-scan.yml | 0 4 * * * | Image vulnerabilities (placeholder scanner) |
| Sunday | 2:00 AM | dependency-security-scan.yml | 0 2 * * 0 | Package audit (wired) |
| Monday | 3:30 AM | sast-scan.yml | 30 3 * * 1 | Code 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.ymlonly 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:
- Grafana dashboards (via OTLP metrics)
- Security Information and Event Management (SIEM)
- Vulnerability management platforms
Related Documentation
- README - CI/CD Overview
- Workflow Triggers
- Release Pipelines
- Path Filters
- CI/CD Recipes
- CI SBOM & Attestation Pipeline
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.mdin the source repo.
