shep

ASPM β€” Application Security Posture Management

Feature 098 adds Shep’s ASPM module: a unified view of application risk across code, dependencies, secrets, containers, cloud, APIs, identity, runtime, compliance, and AI-generated changes. ASPM is anchored on the existing Application entity and integrated through Shep’s TypeSpec-first Clean Architecture conventions β€” no presentation or application file imports from infrastructure/.

This document is the module-level entry point. The TypeSpec source of truth lives in tsp/domain/entities/aspm/ and tsp/domain/value-objects/aspm/, and the generated TypeScript types live in packages/core/src/domain/generated/output.ts. Field listings for every new entity are in ../api/domain-models.md.

Goals

Layering

tsp/domain/entities/aspm/              ← TypeSpec source of truth
tsp/domain/value-objects/aspm/
        β”‚
        β–Ό pnpm tsp:compile
packages/core/src/domain/generated/output.ts   ← never hand-edited
        β”‚
        β–Ό
packages/core/src/domain/aspm/         ← pure logic (scoring, SLA,
                                          ownership resolver, redactor,
                                          dedup key, errors)
        β”‚
        β–Ό
packages/core/src/application/
  β”œβ”€ ports/output/repositories/        ← I<Entity>Repository ports
  β”œβ”€ ports/output/services/            ← IFindingIngestPort,
  β”‚                                       ISbomPort, IExploitIntelPort,
  β”‚                                       ISlaClockPort,
  β”‚                                       IOwnershipYamlReader
  └─ use-cases/aspm/{findings,campaigns,exceptions,posture,
                     ai-review,compliance,ownership}/
        β”‚
        β–Ό
packages/core/src/infrastructure/
  β”œβ”€ persistence/sqlite/migrations/    ← 101–114, idempotent
  β”œβ”€ repositories/aspm/                ← SQLite implementations
  β”œβ”€ services/aspm/                    ← SARIF, CycloneDX, KEV/EPSS,
  β”‚                                       ownership-yaml, system clock
  └─ di/modules/register-aspm.ts       ← tsyringe wiring
        β”‚
        β–Ό
src/presentation/
  β”œβ”€ web/app/aspm/*                    ← Next.js App Router pages
  β”œβ”€ web/app/api/aspm/*                ← SSE posture stream, etc.
  β”œβ”€ web/components/features/aspm/     ← components + colocated stories
  └─ cli/commands/aspm/                ← shep aspm subcommand tree

Lifecycle of a finding

  1. Ingest. A scanner emits a SARIF v2.1.0 document (or a CycloneDX SBOM). shep aspm ingest --sarif file --application <slug> (or the web upload, or an agent-triggered call) resolves IngestFindingsUseCase from the DI container. The use case delegates to IFindingIngestPort (SARIF) or ISbomPort (CycloneDX) β€” both validate with ajv against pinned schemas, enforce a 100MB max-size guard, and walk the validated tree into domain shape.
  2. Redact. Scanner-supplied description and scannerRaw go through the pure-domain Redactor (AWS/GCP/Azure key prefixes, high-entropy strings, common token prefixes, PEM headers). Full raw is SHA-256 hashed; only the hash is stored.
  3. Dedup. findingDedupKey(applicationId, findingDomain, ruleId, locationPath, locationLine, cveId) keys the partial unique index on security_findings. Re-ingestion of the same scanner run is a no-op (NFR-10).
  4. Enrich. IExploitIntelPort.isKev(cveId) and getEpssPercentile(cveId) are looked up from the local-cached KEV and EPSS feeds. Missing data degrades gracefully (null/false).
  5. Score. computeRiskScore(inputs) is a pure function producing (total 0-100, breakdown). The result is appended to risk_scores; the finding’s currentRiskScoreId points at the latest row.
  6. Own. Ownership resolves deterministically: (1) UI override on Application/Service/ApiAsset, (2) .shep/ownership.yaml parsed via IOwnershipYamlReader, (3) Application’s listed owner.
  7. Triage. Findings surface in /aspm/findings, ranked by composite risk score descending, filterable via FindingFilter. Triagers can convert to a WorkItem, declare a RiskException with expiry, or let the campaign engine pick the finding up via its target query.

SLA & exception state machine

SLA state is a pure function of (discoveredAt, canonicalSeverity, SecurityPolicy, ISlaClockPort.now()):

Findings with an Active RiskException are excluded from SLA breach counts until the exception expires. Effective finding state at read time factors in expired exceptions automatically (they transition the finding back to its prior state on the next read).

Risk score

RiskScore.total is 0-100, computed deterministically from the breakdown components:

Weights live as documented constants in domain/aspm/scoring/weights.ts. The fixture-driven golden-file test in tests/unit/domain/aspm/compute-risk-score.golden.test.ts asserts byte-stable output across representative inputs; any weight change must update the fixture intentionally.

AI-change risk review

AiChangeRiskSignal is a separate entity, not a tagged finding β€” keeping SLA math and exception stats clean (research decision 6). Shep’s existing agent infrastructure resolves RecordAiChangeRiskSignalUseCase from the DI container and records a signal post-change. The /aspm/ai-review queue lists Open and Acknowledged signals; reviewers can Dismiss (false-positive) or Graduate (confirmed risk β†’ new SecurityFinding with the signal’s evidence preserved). The use case is agent-agnostic β€” no Anthropic / OpenAI SDK import outside infrastructure.

Compliance

ComplianceControl carries (frameworkId, controlId, title, description) for OWASP ASVS and CWE Top 25 in MVP. Findings link to zero-or-more controls via SARIF taxa references during ingestion. Adding SOC2 / PCI DSS / HIPAA is purely additive content β€” no schema change.

CLI surface

shep aspm (parent) exposes:

Every leaf subcommand is thin β€” argument parsing + use-case call + formatted output. All logic lives in the use cases.

Web surface

Routes under src/presentation/web/app/aspm/*:

Every component under components/features/aspm/ has a colocated *.stories.tsx covering Default / Loading / Error plus the variants in NFR-17 (Critical / High / KEV / Exception / AiGraduated).

Live posture updates stream over /api/aspm/posture/stream using the existing SSE pattern; the dashboard’s PostureCardsLive is the subscriber.

Determinism guarantees

Forward compatibility

ASPM tables ship with a nullable workspace_id column today. When the workspace/permissions subsystem lands, this becomes a backfill β€” not a structural rewrite (research decision 13).