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.
SecurityFinding model with cross-scanner-comparable canonical
severity AND preserved raw severity.RiskScore (CVSS + EPSS + KEV +
exposure + criticality + data classification) as a pure-domain
function. Every score has a breakdown stored for audit..shep/ownership.yaml + UI
overrides.RiskException with expiry and an immutable
audit log.RemediationCampaigns whose progress is computed at
read time./aspm), findings list/detail
(/aspm/findings), campaign board, owners view, compliance coverage,
asset inventory, and an AI-change review queue.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
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.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.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).IExploitIntelPort.isKev(cveId) and
getEpssPercentile(cveId) are looked up from the local-cached KEV
and EPSS feeds. Missing data degrades gracefully (null/false).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..shep/ownership.yaml parsed via
IOwnershipYamlReader, (3) Applicationβs listed owner./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 state is a pure function of (discoveredAt, canonicalSeverity,
SecurityPolicy, ISlaClockPort.now()):
Healthy β elapsed < 50% of the policy window.AtRisk β elapsed in [50%, 100%).Breached β elapsed β₯ 100%.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).
RiskScore.total is 0-100, computed deterministically from the
breakdown components:
cvssContribution β base CVSS v3.1 severity normalized to 0-100.epssContribution β EPSS percentile scaled.kevContribution β flat boost when the CVE is on the KEV catalog.exposureContribution β derived from Application.exposure.criticalityContribution β derived from Application.criticality.dataClassificationContribution β derived from
Application.dataClassification.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.
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.
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.
shep aspm (parent) exposes:
ingest β SARIF or SBOM ingestion with --json for machine-readable
summaries.findings list|show β ranked findings + individual detail.campaigns list|create|close|progress β campaign lifecycle.posture [--app <slug>] β headline posture or per-application posture.exceptions list-expiring β exceptions about to expire.ai-review list|dismiss|graduate β AI-change queue triage.Every leaf subcommand is thin β argument parsing + use-case call + formatted output. All logic lives in the use cases.
Routes under src/presentation/web/app/aspm/*:
/aspm β dashboard (posture cards, 30-day risk trend, live SSE)./aspm/findings, /aspm/findings/[id] β ranked list + detail panel./aspm/inventory β React Flow asset-risk graph./aspm/owners β owner map./aspm/compliance β per-framework coverage view./aspm/ai-review β AI-change review queue.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.
computeRiskScore is pure; identical inputs β identical score.findingDedupKey is pure; re-ingestion of an identical document
inserts zero new rows.computeSlaState, resolveOwnership, and effectiveFindingState are
pure functions over their inputs and ISlaClockPort.now().packages/CLAUDE.md).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).
tsp/.