update evidence bundle to include new evidence types and implement ProofSpine integration
Some checks failed
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
This commit is contained in:
@@ -25,6 +25,24 @@ func (v FindingStatus) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
type MaterialChangeType string
|
||||
|
||||
const (
|
||||
MaterialChangeTypeReachabilityFlip MaterialChangeType = "reachability_flip"
|
||||
MaterialChangeTypeVexFlip MaterialChangeType = "vex_flip"
|
||||
MaterialChangeTypeRangeBoundary MaterialChangeType = "range_boundary"
|
||||
MaterialChangeTypeIntelligenceFlip MaterialChangeType = "intelligence_flip"
|
||||
)
|
||||
|
||||
func (v MaterialChangeType) Validate() error {
|
||||
switch v {
|
||||
case MaterialChangeTypeReachabilityFlip, MaterialChangeTypeVexFlip, MaterialChangeTypeRangeBoundary, MaterialChangeTypeIntelligenceFlip:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid value for MaterialChangeType: %s", string(v))
|
||||
}
|
||||
}
|
||||
|
||||
type PolicyEffect string
|
||||
|
||||
const (
|
||||
@@ -131,6 +149,25 @@ func (v VexStatus) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
type VexStatusType string
|
||||
|
||||
const (
|
||||
VexStatusTypeAffected VexStatusType = "affected"
|
||||
VexStatusTypeNotAffected VexStatusType = "not_affected"
|
||||
VexStatusTypeFixed VexStatusType = "fixed"
|
||||
VexStatusTypeUnderInvestigation VexStatusType = "under_investigation"
|
||||
VexStatusTypeUnknown VexStatusType = "unknown"
|
||||
)
|
||||
|
||||
func (v VexStatusType) Validate() error {
|
||||
switch v {
|
||||
case VexStatusTypeAffected, VexStatusTypeNotAffected, VexStatusTypeFixed, VexStatusTypeUnderInvestigation, VexStatusTypeUnknown:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid value for VexStatusType: %s", string(v))
|
||||
}
|
||||
}
|
||||
|
||||
const BuildProvenanceSchemaVersion = "StellaOps.BuildProvenance@1"
|
||||
|
||||
const CustomEvidenceSchemaVersion = "StellaOps.CustomEvidence@1"
|
||||
@@ -143,6 +180,8 @@ const SbomAttestationSchemaVersion = "StellaOps.SBOMAttestation@1"
|
||||
|
||||
const ScanResultsSchemaVersion = "StellaOps.ScanResults@1"
|
||||
|
||||
const SmartDiffPredicateSchemaVersion = "1.0.0"
|
||||
|
||||
const VexAttestationSchemaVersion = "StellaOps.VEXAttestation@1"
|
||||
|
||||
type BuildMetadata struct {
|
||||
@@ -245,6 +284,61 @@ func (value *CustomProperty) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type DiffHunk struct {
|
||||
StartLine float64 `json:"startLine"`
|
||||
LineCount float64 `json:"lineCount"`
|
||||
Content *string `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
func (value *DiffHunk) Validate() error {
|
||||
if value == nil {
|
||||
return errors.New("DiffHunk is nil")
|
||||
}
|
||||
if value.StartLine < 0 {
|
||||
return fmt.Errorf("DiffHunk.StartLine must be >= 0")
|
||||
}
|
||||
if value.LineCount < 0 {
|
||||
return fmt.Errorf("DiffHunk.LineCount must be >= 0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DiffPayload struct {
|
||||
FilesAdded []string `json:"filesAdded,omitempty"`
|
||||
FilesRemoved []string `json:"filesRemoved,omitempty"`
|
||||
FilesChanged []FileChange `json:"filesChanged,omitempty"`
|
||||
PackagesChanged []PackageChange `json:"packagesChanged,omitempty"`
|
||||
PackagesAdded []PackageRef `json:"packagesAdded,omitempty"`
|
||||
PackagesRemoved []PackageRef `json:"packagesRemoved,omitempty"`
|
||||
}
|
||||
|
||||
func (value *DiffPayload) Validate() error {
|
||||
if value == nil {
|
||||
return errors.New("DiffPayload is nil")
|
||||
}
|
||||
for i := range value.FilesChanged {
|
||||
if err := value.FilesChanged[i].Validate(); err != nil {
|
||||
return fmt.Errorf("invalid DiffPayload.FilesChanged[%d]: %w", i, err)
|
||||
}
|
||||
}
|
||||
for i := range value.PackagesChanged {
|
||||
if err := value.PackagesChanged[i].Validate(); err != nil {
|
||||
return fmt.Errorf("invalid DiffPayload.PackagesChanged[%d]: %w", i, err)
|
||||
}
|
||||
}
|
||||
for i := range value.PackagesAdded {
|
||||
if err := value.PackagesAdded[i].Validate(); err != nil {
|
||||
return fmt.Errorf("invalid DiffPayload.PackagesAdded[%d]: %w", i, err)
|
||||
}
|
||||
}
|
||||
for i := range value.PackagesRemoved {
|
||||
if err := value.PackagesRemoved[i].Validate(); err != nil {
|
||||
return fmt.Errorf("invalid DiffPayload.PackagesRemoved[%d]: %w", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DigestReference struct {
|
||||
Algorithm string `json:"algorithm"`
|
||||
Value string `json:"value"`
|
||||
@@ -274,6 +368,100 @@ func (value *EnvironmentMetadata) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type FileChange struct {
|
||||
Path string `json:"path"`
|
||||
Hunks []DiffHunk `json:"hunks,omitempty"`
|
||||
FromHash *string `json:"fromHash,omitempty"`
|
||||
ToHash *string `json:"toHash,omitempty"`
|
||||
}
|
||||
|
||||
func (value *FileChange) Validate() error {
|
||||
if value == nil {
|
||||
return errors.New("FileChange is nil")
|
||||
}
|
||||
for i := range value.Hunks {
|
||||
if err := value.Hunks[i].Validate(); err != nil {
|
||||
return fmt.Errorf("invalid FileChange.Hunks[%d]: %w", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FindingKey struct {
|
||||
ComponentPurl string `json:"componentPurl"`
|
||||
ComponentVersion string `json:"componentVersion"`
|
||||
CveId string `json:"cveId"`
|
||||
}
|
||||
|
||||
func (value *FindingKey) Validate() error {
|
||||
if value == nil {
|
||||
return errors.New("FindingKey is nil")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ImageReference struct {
|
||||
Digest string `json:"digest"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Tag *string `json:"tag,omitempty"`
|
||||
}
|
||||
|
||||
func (value *ImageReference) Validate() error {
|
||||
if value == nil {
|
||||
return errors.New("ImageReference is nil")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type LicenseDelta struct {
|
||||
Added []string `json:"added,omitempty"`
|
||||
Removed []string `json:"removed,omitempty"`
|
||||
}
|
||||
|
||||
func (value *LicenseDelta) Validate() error {
|
||||
if value == nil {
|
||||
return errors.New("LicenseDelta is nil")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type MaterialChange struct {
|
||||
FindingKey FindingKey `json:"findingKey"`
|
||||
ChangeType MaterialChangeType `json:"changeType"`
|
||||
Reason string `json:"reason"`
|
||||
PreviousState *RiskState `json:"previousState,omitempty"`
|
||||
CurrentState *RiskState `json:"currentState,omitempty"`
|
||||
PriorityScore *float64 `json:"priorityScore,omitempty"`
|
||||
}
|
||||
|
||||
func (value *MaterialChange) Validate() error {
|
||||
if value == nil {
|
||||
return errors.New("MaterialChange is nil")
|
||||
}
|
||||
if err := value.FindingKey.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid MaterialChange.FindingKey: %w", err)
|
||||
}
|
||||
if err := value.ChangeType.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid MaterialChange.ChangeType: %w", err)
|
||||
}
|
||||
if value.PreviousState != nil {
|
||||
if err := value.PreviousState.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid MaterialChange.PreviousState: %w", err)
|
||||
}
|
||||
}
|
||||
if value.CurrentState != nil {
|
||||
if err := value.CurrentState.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid MaterialChange.CurrentState: %w", err)
|
||||
}
|
||||
}
|
||||
if value.PriorityScore != nil {
|
||||
if *value.PriorityScore < 0 {
|
||||
return fmt.Errorf("MaterialChange.PriorityScore must be >= 0")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type MaterialReference struct {
|
||||
Uri string `json:"uri"`
|
||||
Digests []DigestReference `json:"digests"`
|
||||
@@ -295,6 +483,39 @@ func (value *MaterialReference) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type PackageChange struct {
|
||||
Name string `json:"name"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
Purl *string `json:"purl,omitempty"`
|
||||
LicenseDelta *LicenseDelta `json:"licenseDelta,omitempty"`
|
||||
}
|
||||
|
||||
func (value *PackageChange) Validate() error {
|
||||
if value == nil {
|
||||
return errors.New("PackageChange is nil")
|
||||
}
|
||||
if value.LicenseDelta != nil {
|
||||
if err := value.LicenseDelta.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid PackageChange.LicenseDelta: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type PackageRef struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Purl *string `json:"purl,omitempty"`
|
||||
}
|
||||
|
||||
func (value *PackageRef) Validate() error {
|
||||
if value == nil {
|
||||
return errors.New("PackageRef is nil")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type PolicyDecision struct {
|
||||
PolicyId string `json:"policyId"`
|
||||
RuleId string `json:"ruleId"`
|
||||
@@ -340,6 +561,27 @@ func (value *PolicyEvaluation) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReachabilityGate struct {
|
||||
Reachable *bool `json:"reachable,omitempty"`
|
||||
ConfigActivated *bool `json:"configActivated,omitempty"`
|
||||
RunningUser *bool `json:"runningUser,omitempty"`
|
||||
Class float64 `json:"class"`
|
||||
Rationale *string `json:"rationale,omitempty"`
|
||||
}
|
||||
|
||||
func (value *ReachabilityGate) Validate() error {
|
||||
if value == nil {
|
||||
return errors.New("ReachabilityGate is nil")
|
||||
}
|
||||
if value.Class < -1 {
|
||||
return fmt.Errorf("ReachabilityGate.Class must be >= -1")
|
||||
}
|
||||
if value.Class > 7 {
|
||||
return fmt.Errorf("ReachabilityGate.Class must be <= 7")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RiskFactor struct {
|
||||
Name string `json:"name"`
|
||||
Weight float64 `json:"weight"`
|
||||
@@ -392,6 +634,51 @@ func (value *RiskProfileEvidence) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type RiskState struct {
|
||||
Reachable *bool `json:"reachable,omitempty"`
|
||||
VexStatus VexStatusType `json:"vexStatus"`
|
||||
InAffectedRange *bool `json:"inAffectedRange,omitempty"`
|
||||
Kev bool `json:"kev"`
|
||||
EpssScore *float64 `json:"epssScore,omitempty"`
|
||||
PolicyFlags []string `json:"policyFlags,omitempty"`
|
||||
}
|
||||
|
||||
func (value *RiskState) Validate() error {
|
||||
if value == nil {
|
||||
return errors.New("RiskState is nil")
|
||||
}
|
||||
if err := value.VexStatus.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid RiskState.VexStatus: %w", err)
|
||||
}
|
||||
if value.EpssScore != nil {
|
||||
if *value.EpssScore < 0 {
|
||||
return fmt.Errorf("RiskState.EpssScore must be >= 0")
|
||||
}
|
||||
if *value.EpssScore > 1 {
|
||||
return fmt.Errorf("RiskState.EpssScore must be <= 1")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RuntimeContext struct {
|
||||
Entrypoint []string `json:"entrypoint,omitempty"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
User *UserContext `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
func (value *RuntimeContext) Validate() error {
|
||||
if value == nil {
|
||||
return errors.New("RuntimeContext is nil")
|
||||
}
|
||||
if value.User != nil {
|
||||
if err := value.User.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid RuntimeContext.User: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SbomAttestation struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
SubjectDigest string `json:"subjectDigest"`
|
||||
@@ -501,6 +788,94 @@ func (value *ScanResults) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type ScannerInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Ruleset *string `json:"ruleset,omitempty"`
|
||||
}
|
||||
|
||||
func (value *ScannerInfo) Validate() error {
|
||||
if value == nil {
|
||||
return errors.New("ScannerInfo is nil")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SmartDiffPredicate struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
BaseImage ImageReference `json:"baseImage"`
|
||||
TargetImage ImageReference `json:"targetImage"`
|
||||
Diff DiffPayload `json:"diff"`
|
||||
Context *RuntimeContext `json:"context,omitempty"`
|
||||
ReachabilityGate ReachabilityGate `json:"reachabilityGate"`
|
||||
Scanner ScannerInfo `json:"scanner"`
|
||||
SuppressedCount *float64 `json:"suppressedCount,omitempty"`
|
||||
MaterialChanges []MaterialChange `json:"materialChanges,omitempty"`
|
||||
}
|
||||
|
||||
func (value *SmartDiffPredicate) Validate() error {
|
||||
if value == nil {
|
||||
return errors.New("SmartDiffPredicate is nil")
|
||||
}
|
||||
if value.SchemaVersion != "1.0.0" {
|
||||
return fmt.Errorf("SmartDiffPredicate.SchemaVersion must equal 1.0.0")
|
||||
}
|
||||
if err := value.BaseImage.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid SmartDiffPredicate.BaseImage: %w", err)
|
||||
}
|
||||
if err := value.TargetImage.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid SmartDiffPredicate.TargetImage: %w", err)
|
||||
}
|
||||
if err := value.Diff.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid SmartDiffPredicate.Diff: %w", err)
|
||||
}
|
||||
if value.Context != nil {
|
||||
if err := value.Context.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid SmartDiffPredicate.Context: %w", err)
|
||||
}
|
||||
}
|
||||
if err := value.ReachabilityGate.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid SmartDiffPredicate.ReachabilityGate: %w", err)
|
||||
}
|
||||
if err := value.Scanner.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid SmartDiffPredicate.Scanner: %w", err)
|
||||
}
|
||||
if value.SuppressedCount != nil {
|
||||
if *value.SuppressedCount < 0 {
|
||||
return fmt.Errorf("SmartDiffPredicate.SuppressedCount must be >= 0")
|
||||
}
|
||||
}
|
||||
for i := range value.MaterialChanges {
|
||||
if err := value.MaterialChanges[i].Validate(); err != nil {
|
||||
return fmt.Errorf("invalid SmartDiffPredicate.MaterialChanges[%d]: %w", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type UserContext struct {
|
||||
Uid *float64 `json:"uid,omitempty"`
|
||||
Gid *float64 `json:"gid,omitempty"`
|
||||
Caps []string `json:"caps,omitempty"`
|
||||
}
|
||||
|
||||
func (value *UserContext) Validate() error {
|
||||
if value == nil {
|
||||
return errors.New("UserContext is nil")
|
||||
}
|
||||
if value.Uid != nil {
|
||||
if *value.Uid < 0 {
|
||||
return fmt.Errorf("UserContext.Uid must be >= 0")
|
||||
}
|
||||
}
|
||||
if value.Gid != nil {
|
||||
if *value.Gid < 0 {
|
||||
return fmt.Errorf("UserContext.Gid must be >= 0")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type VexAttestation struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
SubjectDigest string `json:"subjectDigest"`
|
||||
@@ -615,6 +990,17 @@ func (value *ScanResults) CanonicalJSON() ([]byte, error) {
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func (value *SmartDiffPredicate) CanonicalJSON() ([]byte, error) {
|
||||
if err := value.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal SmartDiffPredicate: %w", err)
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func (value *VexAttestation) CanonicalJSON() ([]byte, error) {
|
||||
if err := value.Validate(); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
export const FindingStatusValues = Object.freeze(['detected', 'confirmed', 'fixed', 'not_affected'] as const);
|
||||
export type FindingStatus = typeof FindingStatusValues[number];
|
||||
|
||||
export const MaterialChangeTypeValues = Object.freeze(['reachability_flip', 'vex_flip', 'range_boundary', 'intelligence_flip'] as const);
|
||||
export type MaterialChangeType = typeof MaterialChangeTypeValues[number];
|
||||
|
||||
export const PolicyEffectValues = Object.freeze(['allow', 'deny', 'warn'] as const);
|
||||
export type PolicyEffect = typeof PolicyEffectValues[number];
|
||||
|
||||
@@ -24,6 +27,9 @@ export type Severity = typeof SeverityValues[number];
|
||||
export const VexStatusValues = Object.freeze(['not_affected', 'affected', 'under_investigation', 'fixed'] as const);
|
||||
export type VexStatus = typeof VexStatusValues[number];
|
||||
|
||||
export const VexStatusTypeValues = Object.freeze(['affected', 'not_affected', 'fixed', 'under_investigation', 'unknown'] as const);
|
||||
export type VexStatusType = typeof VexStatusTypeValues[number];
|
||||
|
||||
export interface BuildMetadata {
|
||||
buildStartedOn: string;
|
||||
buildFinishedOn: string;
|
||||
@@ -59,6 +65,21 @@ export interface CustomProperty {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface DiffHunk {
|
||||
startLine: number;
|
||||
lineCount: number;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
export interface DiffPayload {
|
||||
filesAdded?: Array<string>;
|
||||
filesRemoved?: Array<string>;
|
||||
filesChanged?: Array<FileChange>;
|
||||
packagesChanged?: Array<PackageChange>;
|
||||
packagesAdded?: Array<PackageRef>;
|
||||
packagesRemoved?: Array<PackageRef>;
|
||||
}
|
||||
|
||||
export interface DigestReference {
|
||||
algorithm: string;
|
||||
value: string;
|
||||
@@ -69,12 +90,59 @@ export interface EnvironmentMetadata {
|
||||
imageDigest?: DigestReference;
|
||||
}
|
||||
|
||||
export interface FileChange {
|
||||
path: string;
|
||||
hunks?: Array<DiffHunk>;
|
||||
fromHash?: string;
|
||||
toHash?: string;
|
||||
}
|
||||
|
||||
export interface FindingKey {
|
||||
componentPurl: string;
|
||||
componentVersion: string;
|
||||
cveId: string;
|
||||
}
|
||||
|
||||
export interface ImageReference {
|
||||
digest: string;
|
||||
name?: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
export interface LicenseDelta {
|
||||
added?: Array<string>;
|
||||
removed?: Array<string>;
|
||||
}
|
||||
|
||||
export interface MaterialChange {
|
||||
findingKey: FindingKey;
|
||||
changeType: MaterialChangeType;
|
||||
reason: string;
|
||||
previousState?: RiskState;
|
||||
currentState?: RiskState;
|
||||
priorityScore?: number;
|
||||
}
|
||||
|
||||
export interface MaterialReference {
|
||||
uri: string;
|
||||
digests: Array<DigestReference>;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface PackageChange {
|
||||
name: string;
|
||||
from: string;
|
||||
to: string;
|
||||
purl?: string;
|
||||
licenseDelta?: LicenseDelta;
|
||||
}
|
||||
|
||||
export interface PackageRef {
|
||||
name: string;
|
||||
version: string;
|
||||
purl?: string;
|
||||
}
|
||||
|
||||
export interface PolicyDecision {
|
||||
policyId: string;
|
||||
ruleId: string;
|
||||
@@ -92,6 +160,14 @@ export interface PolicyEvaluation {
|
||||
decisions: Array<PolicyDecision>;
|
||||
}
|
||||
|
||||
export interface ReachabilityGate {
|
||||
reachable?: boolean;
|
||||
configActivated?: boolean;
|
||||
runningUser?: boolean;
|
||||
class: number;
|
||||
rationale?: string;
|
||||
}
|
||||
|
||||
export interface RiskFactor {
|
||||
name: string;
|
||||
weight: number;
|
||||
@@ -107,6 +183,21 @@ export interface RiskProfileEvidence {
|
||||
factors: Array<RiskFactor>;
|
||||
}
|
||||
|
||||
export interface RiskState {
|
||||
reachable?: boolean;
|
||||
vexStatus: VexStatusType;
|
||||
inAffectedRange?: boolean;
|
||||
kev: boolean;
|
||||
epssScore?: number;
|
||||
policyFlags?: Array<string>;
|
||||
}
|
||||
|
||||
export interface RuntimeContext {
|
||||
entrypoint?: Array<string>;
|
||||
env?: Record<string, string>;
|
||||
user?: UserContext;
|
||||
}
|
||||
|
||||
export interface SbomAttestation {
|
||||
schemaVersion: 'StellaOps.SBOMAttestation@1';
|
||||
subjectDigest: string;
|
||||
@@ -143,6 +234,30 @@ export interface ScanResults {
|
||||
findings: Array<ScanFinding>;
|
||||
}
|
||||
|
||||
export interface ScannerInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
ruleset?: string;
|
||||
}
|
||||
|
||||
export interface SmartDiffPredicate {
|
||||
schemaVersion: '1.0.0';
|
||||
baseImage: ImageReference;
|
||||
targetImage: ImageReference;
|
||||
diff: DiffPayload;
|
||||
context?: RuntimeContext;
|
||||
reachabilityGate: ReachabilityGate;
|
||||
scanner: ScannerInfo;
|
||||
suppressedCount?: number;
|
||||
materialChanges?: Array<MaterialChange>;
|
||||
}
|
||||
|
||||
export interface UserContext {
|
||||
uid?: number;
|
||||
gid?: number;
|
||||
caps?: Array<string>;
|
||||
}
|
||||
|
||||
export interface VexAttestation {
|
||||
schemaVersion: 'StellaOps.VEXAttestation@1';
|
||||
subjectDigest: string;
|
||||
@@ -324,6 +439,93 @@ function assertCustomProperty(value: unknown, path: string[]): asserts value is
|
||||
}
|
||||
}
|
||||
|
||||
function assertDiffHunk(value: unknown, path: string[]): asserts value is DiffHunk {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
}
|
||||
if (value.startLine === undefined) {
|
||||
throw new Error(`${pathString([...path, 'startLine'])} is required.`);
|
||||
}
|
||||
if (typeof value.startLine !== 'number') {
|
||||
throw new Error(`${pathString([...path, 'startLine'])} must be a number.`);
|
||||
}
|
||||
if (value.startLine < 0) {
|
||||
throw new Error(`${pathString([...path, 'startLine'])} must be >= 0`);
|
||||
}
|
||||
if (value.lineCount === undefined) {
|
||||
throw new Error(`${pathString([...path, 'lineCount'])} is required.`);
|
||||
}
|
||||
if (typeof value.lineCount !== 'number') {
|
||||
throw new Error(`${pathString([...path, 'lineCount'])} must be a number.`);
|
||||
}
|
||||
if (value.lineCount < 0) {
|
||||
throw new Error(`${pathString([...path, 'lineCount'])} must be >= 0`);
|
||||
}
|
||||
if (value.content !== undefined) {
|
||||
if (typeof value.content !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'content'])} must be a string.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertDiffPayload(value: unknown, path: string[]): asserts value is DiffPayload {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
}
|
||||
if (value.filesAdded !== undefined) {
|
||||
if (!Array.isArray(value.filesAdded)) {
|
||||
throw new Error(`${pathString([...path, 'filesAdded'])} must be an array.`);
|
||||
}
|
||||
for (let i = 0; i < value.filesAdded.length; i += 1) {
|
||||
if (typeof value.filesAdded[i] !== 'string') {
|
||||
throw new Error(`${pathString([...[...path, 'filesAdded'], String(i)])} must be a string.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value.filesRemoved !== undefined) {
|
||||
if (!Array.isArray(value.filesRemoved)) {
|
||||
throw new Error(`${pathString([...path, 'filesRemoved'])} must be an array.`);
|
||||
}
|
||||
for (let i = 0; i < value.filesRemoved.length; i += 1) {
|
||||
if (typeof value.filesRemoved[i] !== 'string') {
|
||||
throw new Error(`${pathString([...[...path, 'filesRemoved'], String(i)])} must be a string.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value.filesChanged !== undefined) {
|
||||
if (!Array.isArray(value.filesChanged)) {
|
||||
throw new Error(`${pathString([...path, 'filesChanged'])} must be an array.`);
|
||||
}
|
||||
for (let i = 0; i < value.filesChanged.length; i += 1) {
|
||||
assertFileChange(value.filesChanged[i], [...[...path, 'filesChanged'], String(i)]);
|
||||
}
|
||||
}
|
||||
if (value.packagesChanged !== undefined) {
|
||||
if (!Array.isArray(value.packagesChanged)) {
|
||||
throw new Error(`${pathString([...path, 'packagesChanged'])} must be an array.`);
|
||||
}
|
||||
for (let i = 0; i < value.packagesChanged.length; i += 1) {
|
||||
assertPackageChange(value.packagesChanged[i], [...[...path, 'packagesChanged'], String(i)]);
|
||||
}
|
||||
}
|
||||
if (value.packagesAdded !== undefined) {
|
||||
if (!Array.isArray(value.packagesAdded)) {
|
||||
throw new Error(`${pathString([...path, 'packagesAdded'])} must be an array.`);
|
||||
}
|
||||
for (let i = 0; i < value.packagesAdded.length; i += 1) {
|
||||
assertPackageRef(value.packagesAdded[i], [...[...path, 'packagesAdded'], String(i)]);
|
||||
}
|
||||
}
|
||||
if (value.packagesRemoved !== undefined) {
|
||||
if (!Array.isArray(value.packagesRemoved)) {
|
||||
throw new Error(`${pathString([...path, 'packagesRemoved'])} must be an array.`);
|
||||
}
|
||||
for (let i = 0; i < value.packagesRemoved.length; i += 1) {
|
||||
assertPackageRef(value.packagesRemoved[i], [...[...path, 'packagesRemoved'], String(i)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertDigestReference(value: unknown, path: string[]): asserts value is DigestReference {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
@@ -359,6 +561,147 @@ function assertEnvironmentMetadata(value: unknown, path: string[]): asserts valu
|
||||
}
|
||||
}
|
||||
|
||||
function assertFileChange(value: unknown, path: string[]): asserts value is FileChange {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
}
|
||||
if (value.path === undefined) {
|
||||
throw new Error(`${pathString([...path, 'path'])} is required.`);
|
||||
}
|
||||
if (typeof value.path !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'path'])} must be a string.`);
|
||||
}
|
||||
if (value.hunks !== undefined) {
|
||||
if (!Array.isArray(value.hunks)) {
|
||||
throw new Error(`${pathString([...path, 'hunks'])} must be an array.`);
|
||||
}
|
||||
for (let i = 0; i < value.hunks.length; i += 1) {
|
||||
assertDiffHunk(value.hunks[i], [...[...path, 'hunks'], String(i)]);
|
||||
}
|
||||
}
|
||||
if (value.fromHash !== undefined) {
|
||||
if (typeof value.fromHash !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'fromHash'])} must be a string.`);
|
||||
}
|
||||
}
|
||||
if (value.toHash !== undefined) {
|
||||
if (typeof value.toHash !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'toHash'])} must be a string.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertFindingKey(value: unknown, path: string[]): asserts value is FindingKey {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
}
|
||||
if (value.componentPurl === undefined) {
|
||||
throw new Error(`${pathString([...path, 'componentPurl'])} is required.`);
|
||||
}
|
||||
if (typeof value.componentPurl !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'componentPurl'])} must be a string.`);
|
||||
}
|
||||
if (value.componentVersion === undefined) {
|
||||
throw new Error(`${pathString([...path, 'componentVersion'])} is required.`);
|
||||
}
|
||||
if (typeof value.componentVersion !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'componentVersion'])} must be a string.`);
|
||||
}
|
||||
if (value.cveId === undefined) {
|
||||
throw new Error(`${pathString([...path, 'cveId'])} is required.`);
|
||||
}
|
||||
if (typeof value.cveId !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'cveId'])} must be a string.`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertImageReference(value: unknown, path: string[]): asserts value is ImageReference {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
}
|
||||
if (value.digest === undefined) {
|
||||
throw new Error(`${pathString([...path, 'digest'])} is required.`);
|
||||
}
|
||||
if (typeof value.digest !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'digest'])} must be a string.`);
|
||||
}
|
||||
if (!/^sha256:[A-Fa-f0-9]{64}$/.test(value.digest)) {
|
||||
throw new Error(`${pathString([...path, 'digest'])} does not match expected format.`);
|
||||
}
|
||||
if (value.name !== undefined) {
|
||||
if (typeof value.name !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'name'])} must be a string.`);
|
||||
}
|
||||
}
|
||||
if (value.tag !== undefined) {
|
||||
if (typeof value.tag !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'tag'])} must be a string.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertLicenseDelta(value: unknown, path: string[]): asserts value is LicenseDelta {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
}
|
||||
if (value.added !== undefined) {
|
||||
if (!Array.isArray(value.added)) {
|
||||
throw new Error(`${pathString([...path, 'added'])} must be an array.`);
|
||||
}
|
||||
for (let i = 0; i < value.added.length; i += 1) {
|
||||
if (typeof value.added[i] !== 'string') {
|
||||
throw new Error(`${pathString([...[...path, 'added'], String(i)])} must be a string.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value.removed !== undefined) {
|
||||
if (!Array.isArray(value.removed)) {
|
||||
throw new Error(`${pathString([...path, 'removed'])} must be an array.`);
|
||||
}
|
||||
for (let i = 0; i < value.removed.length; i += 1) {
|
||||
if (typeof value.removed[i] !== 'string') {
|
||||
throw new Error(`${pathString([...[...path, 'removed'], String(i)])} must be a string.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertMaterialChange(value: unknown, path: string[]): asserts value is MaterialChange {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
}
|
||||
if (value.findingKey === undefined) {
|
||||
throw new Error(`${pathString([...path, 'findingKey'])} is required.`);
|
||||
}
|
||||
assertFindingKey(value.findingKey, [...path, 'findingKey']);
|
||||
if (value.changeType === undefined) {
|
||||
throw new Error(`${pathString([...path, 'changeType'])} is required.`);
|
||||
}
|
||||
if (!MaterialChangeTypeValues.includes(value.changeType as MaterialChangeType)) {
|
||||
throw new Error(`${pathString([...path, 'changeType'])} must be one of ${MaterialChangeTypeValues.join(', ')}`);
|
||||
}
|
||||
if (value.reason === undefined) {
|
||||
throw new Error(`${pathString([...path, 'reason'])} is required.`);
|
||||
}
|
||||
if (typeof value.reason !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'reason'])} must be a string.`);
|
||||
}
|
||||
if (value.previousState !== undefined) {
|
||||
assertRiskState(value.previousState, [...path, 'previousState']);
|
||||
}
|
||||
if (value.currentState !== undefined) {
|
||||
assertRiskState(value.currentState, [...path, 'currentState']);
|
||||
}
|
||||
if (value.priorityScore !== undefined) {
|
||||
if (typeof value.priorityScore !== 'number') {
|
||||
throw new Error(`${pathString([...path, 'priorityScore'])} must be a number.`);
|
||||
}
|
||||
if (value.priorityScore < 0) {
|
||||
throw new Error(`${pathString([...path, 'priorityScore'])} must be >= 0`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertMaterialReference(value: unknown, path: string[]): asserts value is MaterialReference {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
@@ -388,6 +731,61 @@ function assertMaterialReference(value: unknown, path: string[]): asserts value
|
||||
}
|
||||
}
|
||||
|
||||
function assertPackageChange(value: unknown, path: string[]): asserts value is PackageChange {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
}
|
||||
if (value.name === undefined) {
|
||||
throw new Error(`${pathString([...path, 'name'])} is required.`);
|
||||
}
|
||||
if (typeof value.name !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'name'])} must be a string.`);
|
||||
}
|
||||
if (value.from === undefined) {
|
||||
throw new Error(`${pathString([...path, 'from'])} is required.`);
|
||||
}
|
||||
if (typeof value.from !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'from'])} must be a string.`);
|
||||
}
|
||||
if (value.to === undefined) {
|
||||
throw new Error(`${pathString([...path, 'to'])} is required.`);
|
||||
}
|
||||
if (typeof value.to !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'to'])} must be a string.`);
|
||||
}
|
||||
if (value.purl !== undefined) {
|
||||
if (typeof value.purl !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'purl'])} must be a string.`);
|
||||
}
|
||||
}
|
||||
if (value.licenseDelta !== undefined) {
|
||||
assertLicenseDelta(value.licenseDelta, [...path, 'licenseDelta']);
|
||||
}
|
||||
}
|
||||
|
||||
function assertPackageRef(value: unknown, path: string[]): asserts value is PackageRef {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
}
|
||||
if (value.name === undefined) {
|
||||
throw new Error(`${pathString([...path, 'name'])} is required.`);
|
||||
}
|
||||
if (typeof value.name !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'name'])} must be a string.`);
|
||||
}
|
||||
if (value.version === undefined) {
|
||||
throw new Error(`${pathString([...path, 'version'])} is required.`);
|
||||
}
|
||||
if (typeof value.version !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'version'])} must be a string.`);
|
||||
}
|
||||
if (value.purl !== undefined) {
|
||||
if (typeof value.purl !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'purl'])} must be a string.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertPolicyDecision(value: unknown, path: string[]): asserts value is PolicyDecision {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
@@ -473,6 +871,44 @@ function assertPolicyEvaluation(value: unknown, path: string[]): asserts value i
|
||||
}
|
||||
}
|
||||
|
||||
function assertReachabilityGate(value: unknown, path: string[]): asserts value is ReachabilityGate {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
}
|
||||
if (value.reachable !== undefined) {
|
||||
if (typeof value.reachable !== 'boolean') {
|
||||
throw new Error(`${pathString([...path, 'reachable'])} must be a boolean.`);
|
||||
}
|
||||
}
|
||||
if (value.configActivated !== undefined) {
|
||||
if (typeof value.configActivated !== 'boolean') {
|
||||
throw new Error(`${pathString([...path, 'configActivated'])} must be a boolean.`);
|
||||
}
|
||||
}
|
||||
if (value.runningUser !== undefined) {
|
||||
if (typeof value.runningUser !== 'boolean') {
|
||||
throw new Error(`${pathString([...path, 'runningUser'])} must be a boolean.`);
|
||||
}
|
||||
}
|
||||
if (value.class === undefined) {
|
||||
throw new Error(`${pathString([...path, 'class'])} is required.`);
|
||||
}
|
||||
if (typeof value.class !== 'number') {
|
||||
throw new Error(`${pathString([...path, 'class'])} must be a number.`);
|
||||
}
|
||||
if (value.class < -1) {
|
||||
throw new Error(`${pathString([...path, 'class'])} must be >= -1`);
|
||||
}
|
||||
if (value.class > 7) {
|
||||
throw new Error(`${pathString([...path, 'class'])} must be <= 7`);
|
||||
}
|
||||
if (value.rationale !== undefined) {
|
||||
if (typeof value.rationale !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'rationale'])} must be a string.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertRiskFactor(value: unknown, path: string[]): asserts value is RiskFactor {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
@@ -559,6 +995,86 @@ function assertRiskProfileEvidence(value: unknown, path: string[]): asserts valu
|
||||
}
|
||||
}
|
||||
|
||||
function assertRiskState(value: unknown, path: string[]): asserts value is RiskState {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
}
|
||||
if (value.reachable !== undefined) {
|
||||
if (typeof value.reachable !== 'boolean') {
|
||||
throw new Error(`${pathString([...path, 'reachable'])} must be a boolean.`);
|
||||
}
|
||||
}
|
||||
if (value.vexStatus === undefined) {
|
||||
throw new Error(`${pathString([...path, 'vexStatus'])} is required.`);
|
||||
}
|
||||
if (!VexStatusTypeValues.includes(value.vexStatus as VexStatusType)) {
|
||||
throw new Error(`${pathString([...path, 'vexStatus'])} must be one of ${VexStatusTypeValues.join(', ')}`);
|
||||
}
|
||||
if (value.inAffectedRange !== undefined) {
|
||||
if (typeof value.inAffectedRange !== 'boolean') {
|
||||
throw new Error(`${pathString([...path, 'inAffectedRange'])} must be a boolean.`);
|
||||
}
|
||||
}
|
||||
if (value.kev === undefined) {
|
||||
throw new Error(`${pathString([...path, 'kev'])} is required.`);
|
||||
}
|
||||
if (typeof value.kev !== 'boolean') {
|
||||
throw new Error(`${pathString([...path, 'kev'])} must be a boolean.`);
|
||||
}
|
||||
if (value.epssScore !== undefined) {
|
||||
if (typeof value.epssScore !== 'number') {
|
||||
throw new Error(`${pathString([...path, 'epssScore'])} must be a number.`);
|
||||
}
|
||||
if (value.epssScore < 0) {
|
||||
throw new Error(`${pathString([...path, 'epssScore'])} must be >= 0`);
|
||||
}
|
||||
if (value.epssScore > 1) {
|
||||
throw new Error(`${pathString([...path, 'epssScore'])} must be <= 1`);
|
||||
}
|
||||
}
|
||||
if (value.policyFlags !== undefined) {
|
||||
if (!Array.isArray(value.policyFlags)) {
|
||||
throw new Error(`${pathString([...path, 'policyFlags'])} must be an array.`);
|
||||
}
|
||||
for (let i = 0; i < value.policyFlags.length; i += 1) {
|
||||
if (typeof value.policyFlags[i] !== 'string') {
|
||||
throw new Error(`${pathString([...[...path, 'policyFlags'], String(i)])} must be a string.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertRuntimeContext(value: unknown, path: string[]): asserts value is RuntimeContext {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
}
|
||||
if (value.entrypoint !== undefined) {
|
||||
if (!Array.isArray(value.entrypoint)) {
|
||||
throw new Error(`${pathString([...path, 'entrypoint'])} must be an array.`);
|
||||
}
|
||||
for (let i = 0; i < value.entrypoint.length; i += 1) {
|
||||
if (typeof value.entrypoint[i] !== 'string') {
|
||||
throw new Error(`${pathString([...[...path, 'entrypoint'], String(i)])} must be a string.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value.env !== undefined) {
|
||||
if (!isRecord(value.env)) {
|
||||
throw new Error(`${pathString([...path, 'env'])} must be an object.`);
|
||||
}
|
||||
for (const key of Object.keys(value.env)) {
|
||||
const entry = (value.env as Record<string, unknown>)[key];
|
||||
const entryPath = [...[...path, 'env'], key];
|
||||
if (typeof entry !== 'string') {
|
||||
throw new Error(`${pathString(entryPath)} must be a string.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value.user !== undefined) {
|
||||
assertUserContext(value.user, [...path, 'user']);
|
||||
}
|
||||
}
|
||||
|
||||
function assertSbomAttestation(value: unknown, path: string[]): asserts value is SbomAttestation {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
@@ -760,6 +1276,115 @@ function assertScanResults(value: unknown, path: string[]): asserts value is Sca
|
||||
}
|
||||
}
|
||||
|
||||
function assertScannerInfo(value: unknown, path: string[]): asserts value is ScannerInfo {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
}
|
||||
if (value.name === undefined) {
|
||||
throw new Error(`${pathString([...path, 'name'])} is required.`);
|
||||
}
|
||||
if (typeof value.name !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'name'])} must be a string.`);
|
||||
}
|
||||
if (value.version === undefined) {
|
||||
throw new Error(`${pathString([...path, 'version'])} is required.`);
|
||||
}
|
||||
if (typeof value.version !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'version'])} must be a string.`);
|
||||
}
|
||||
if (value.ruleset !== undefined) {
|
||||
if (typeof value.ruleset !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'ruleset'])} must be a string.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertSmartDiffPredicate(value: unknown, path: string[]): asserts value is SmartDiffPredicate {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
}
|
||||
if (value.schemaVersion === undefined) {
|
||||
throw new Error(`${pathString([...path, 'schemaVersion'])} is required.`);
|
||||
}
|
||||
if (typeof value.schemaVersion !== 'string') {
|
||||
throw new Error(`${pathString([...path, 'schemaVersion'])} must be a string.`);
|
||||
}
|
||||
if (value.schemaVersion !== '1.0.0') {
|
||||
throw new Error(`${pathString([...path, 'schemaVersion'])} must equal '1.0.0'.`);
|
||||
}
|
||||
if (value.baseImage === undefined) {
|
||||
throw new Error(`${pathString([...path, 'baseImage'])} is required.`);
|
||||
}
|
||||
assertImageReference(value.baseImage, [...path, 'baseImage']);
|
||||
if (value.targetImage === undefined) {
|
||||
throw new Error(`${pathString([...path, 'targetImage'])} is required.`);
|
||||
}
|
||||
assertImageReference(value.targetImage, [...path, 'targetImage']);
|
||||
if (value.diff === undefined) {
|
||||
throw new Error(`${pathString([...path, 'diff'])} is required.`);
|
||||
}
|
||||
assertDiffPayload(value.diff, [...path, 'diff']);
|
||||
if (value.context !== undefined) {
|
||||
assertRuntimeContext(value.context, [...path, 'context']);
|
||||
}
|
||||
if (value.reachabilityGate === undefined) {
|
||||
throw new Error(`${pathString([...path, 'reachabilityGate'])} is required.`);
|
||||
}
|
||||
assertReachabilityGate(value.reachabilityGate, [...path, 'reachabilityGate']);
|
||||
if (value.scanner === undefined) {
|
||||
throw new Error(`${pathString([...path, 'scanner'])} is required.`);
|
||||
}
|
||||
assertScannerInfo(value.scanner, [...path, 'scanner']);
|
||||
if (value.suppressedCount !== undefined) {
|
||||
if (typeof value.suppressedCount !== 'number') {
|
||||
throw new Error(`${pathString([...path, 'suppressedCount'])} must be a number.`);
|
||||
}
|
||||
if (value.suppressedCount < 0) {
|
||||
throw new Error(`${pathString([...path, 'suppressedCount'])} must be >= 0`);
|
||||
}
|
||||
}
|
||||
if (value.materialChanges !== undefined) {
|
||||
if (!Array.isArray(value.materialChanges)) {
|
||||
throw new Error(`${pathString([...path, 'materialChanges'])} must be an array.`);
|
||||
}
|
||||
for (let i = 0; i < value.materialChanges.length; i += 1) {
|
||||
assertMaterialChange(value.materialChanges[i], [...[...path, 'materialChanges'], String(i)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertUserContext(value: unknown, path: string[]): asserts value is UserContext {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
}
|
||||
if (value.uid !== undefined) {
|
||||
if (typeof value.uid !== 'number') {
|
||||
throw new Error(`${pathString([...path, 'uid'])} must be a number.`);
|
||||
}
|
||||
if (value.uid < 0) {
|
||||
throw new Error(`${pathString([...path, 'uid'])} must be >= 0`);
|
||||
}
|
||||
}
|
||||
if (value.gid !== undefined) {
|
||||
if (typeof value.gid !== 'number') {
|
||||
throw new Error(`${pathString([...path, 'gid'])} must be a number.`);
|
||||
}
|
||||
if (value.gid < 0) {
|
||||
throw new Error(`${pathString([...path, 'gid'])} must be >= 0`);
|
||||
}
|
||||
}
|
||||
if (value.caps !== undefined) {
|
||||
if (!Array.isArray(value.caps)) {
|
||||
throw new Error(`${pathString([...path, 'caps'])} must be an array.`);
|
||||
}
|
||||
for (let i = 0; i < value.caps.length; i += 1) {
|
||||
if (typeof value.caps[i] !== 'string') {
|
||||
throw new Error(`${pathString([...[...path, 'caps'], String(i)])} must be a string.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertVexAttestation(value: unknown, path: string[]): asserts value is VexAttestation {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${pathString(path)} must be an object.`);
|
||||
@@ -914,6 +1539,16 @@ export function canonicalizeScanResults(value: ScanResults): string {
|
||||
return canonicalStringify(value);
|
||||
}
|
||||
|
||||
export function validateSmartDiffPredicate(value: unknown): SmartDiffPredicate {
|
||||
assertSmartDiffPredicate(value, []);
|
||||
return value as SmartDiffPredicate;
|
||||
}
|
||||
|
||||
export function canonicalizeSmartDiffPredicate(value: SmartDiffPredicate): string {
|
||||
assertSmartDiffPredicate(value, []);
|
||||
return canonicalStringify(value);
|
||||
}
|
||||
|
||||
export function validateVexAttestation(value: unknown): VexAttestation {
|
||||
assertVexAttestation(value, []);
|
||||
return value as VexAttestation;
|
||||
|
||||
@@ -0,0 +1,501 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://stella-ops.org/schemas/attestor/stellaops-smart-diff.v1.json",
|
||||
"title": "Smart-Diff predicate describing differential analysis between two scans.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"baseImage",
|
||||
"targetImage",
|
||||
"diff",
|
||||
"reachabilityGate",
|
||||
"scanner"
|
||||
],
|
||||
"properties": {
|
||||
"schemaVersion": {
|
||||
"type": "string",
|
||||
"const": "1.0.0",
|
||||
"description": "Schema version (semver)."
|
||||
},
|
||||
"baseImage": {
|
||||
"$ref": "#/$defs/ImageReference",
|
||||
"description": "Base scan image reference."
|
||||
},
|
||||
"targetImage": {
|
||||
"$ref": "#/$defs/ImageReference",
|
||||
"description": "Target scan image reference."
|
||||
},
|
||||
"diff": {
|
||||
"$ref": "#/$defs/DiffPayload",
|
||||
"description": "Diff payload between base and target."
|
||||
},
|
||||
"context": {
|
||||
"$ref": "#/$defs/RuntimeContext",
|
||||
"description": "Optional runtime context."
|
||||
},
|
||||
"reachabilityGate": {
|
||||
"$ref": "#/$defs/ReachabilityGate",
|
||||
"description": "Derived reachability gate."
|
||||
},
|
||||
"scanner": {
|
||||
"$ref": "#/$defs/ScannerInfo",
|
||||
"description": "Scanner identity."
|
||||
},
|
||||
"suppressedCount": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Number of findings suppressed by pre-filters."
|
||||
},
|
||||
"materialChanges": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/MaterialChange"
|
||||
},
|
||||
"minItems": 0,
|
||||
"description": "Optional list of material changes."
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"ImageReference": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Reference to a container image.",
|
||||
"required": [
|
||||
"digest"
|
||||
],
|
||||
"properties": {
|
||||
"digest": {
|
||||
"type": "string",
|
||||
"pattern": "^sha256:[A-Fa-f0-9]{64}$",
|
||||
"description": "Image digest."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Image name."
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"description": "Image tag."
|
||||
}
|
||||
}
|
||||
},
|
||||
"DiffHunk": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Single diff hunk for a file change.",
|
||||
"required": [
|
||||
"startLine",
|
||||
"lineCount"
|
||||
],
|
||||
"properties": {
|
||||
"startLine": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Start line number."
|
||||
},
|
||||
"lineCount": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Number of lines in the hunk."
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Optional hunk content."
|
||||
}
|
||||
}
|
||||
},
|
||||
"FileChange": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "File-level delta captured by Smart-Diff.",
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "File path."
|
||||
},
|
||||
"hunks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/DiffHunk"
|
||||
},
|
||||
"minItems": 0,
|
||||
"description": "Optional hunks describing the file change."
|
||||
},
|
||||
"fromHash": {
|
||||
"type": "string",
|
||||
"description": "Previous file hash (when available)."
|
||||
},
|
||||
"toHash": {
|
||||
"type": "string",
|
||||
"description": "Current file hash (when available)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"LicenseDelta": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "License delta for a package or file.",
|
||||
"properties": {
|
||||
"added": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 0,
|
||||
"description": "Licenses added."
|
||||
},
|
||||
"removed": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 0,
|
||||
"description": "Licenses removed."
|
||||
}
|
||||
}
|
||||
},
|
||||
"PackageChange": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Package version change between the base and target scan.",
|
||||
"required": [
|
||||
"name",
|
||||
"from",
|
||||
"to"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Package name."
|
||||
},
|
||||
"from": {
|
||||
"type": "string",
|
||||
"description": "Previous package version."
|
||||
},
|
||||
"to": {
|
||||
"type": "string",
|
||||
"description": "Current package version."
|
||||
},
|
||||
"purl": {
|
||||
"type": "string",
|
||||
"description": "Package URL (purl)."
|
||||
},
|
||||
"licenseDelta": {
|
||||
"$ref": "#/$defs/LicenseDelta",
|
||||
"description": "License delta between versions."
|
||||
}
|
||||
}
|
||||
},
|
||||
"PackageRef": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Package reference used in diffs.",
|
||||
"required": [
|
||||
"name",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Package name."
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Package version."
|
||||
},
|
||||
"purl": {
|
||||
"type": "string",
|
||||
"description": "Package URL (purl)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"DiffPayload": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Diff payload describing file and package deltas.",
|
||||
"properties": {
|
||||
"filesAdded": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 0,
|
||||
"description": "Paths of files added."
|
||||
},
|
||||
"filesRemoved": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 0,
|
||||
"description": "Paths of files removed."
|
||||
},
|
||||
"filesChanged": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/FileChange"
|
||||
},
|
||||
"minItems": 0,
|
||||
"description": "Collection of file changes."
|
||||
},
|
||||
"packagesChanged": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/PackageChange"
|
||||
},
|
||||
"minItems": 0,
|
||||
"description": "Collection of package changes."
|
||||
},
|
||||
"packagesAdded": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/PackageRef"
|
||||
},
|
||||
"minItems": 0,
|
||||
"description": "Packages added."
|
||||
},
|
||||
"packagesRemoved": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/PackageRef"
|
||||
},
|
||||
"minItems": 0,
|
||||
"description": "Packages removed."
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserContext": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Runtime user context for the image.",
|
||||
"properties": {
|
||||
"uid": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "User ID."
|
||||
},
|
||||
"gid": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Group ID."
|
||||
},
|
||||
"caps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 0,
|
||||
"description": "Linux capabilities (string names)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"RuntimeContext": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Runtime context used for reachability gating and policy decisions.",
|
||||
"properties": {
|
||||
"entrypoint": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 0,
|
||||
"description": "Entrypoint command array."
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Environment variables map."
|
||||
},
|
||||
"user": {
|
||||
"$ref": "#/$defs/UserContext",
|
||||
"description": "Runtime user context."
|
||||
}
|
||||
}
|
||||
},
|
||||
"ReachabilityGate": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "3-bit reachability gate derived from the 7-state lattice.",
|
||||
"required": [
|
||||
"class"
|
||||
],
|
||||
"properties": {
|
||||
"reachable": {
|
||||
"type": "boolean",
|
||||
"description": "True/false if reachability is known; absent indicates unknown."
|
||||
},
|
||||
"configActivated": {
|
||||
"type": "boolean",
|
||||
"description": "True if configuration activates the finding."
|
||||
},
|
||||
"runningUser": {
|
||||
"type": "boolean",
|
||||
"description": "True if running user enables the finding."
|
||||
},
|
||||
"class": {
|
||||
"type": "integer",
|
||||
"minimum": -1,
|
||||
"maximum": 7,
|
||||
"description": "Derived 3-bit class (0..7), or -1 if any bit is unknown."
|
||||
},
|
||||
"rationale": {
|
||||
"type": "string",
|
||||
"description": "Optional human-readable rationale for the gate."
|
||||
}
|
||||
}
|
||||
},
|
||||
"ScannerInfo": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Scanner identity and ruleset information.",
|
||||
"required": [
|
||||
"name",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Scanner name."
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Scanner version string."
|
||||
},
|
||||
"ruleset": {
|
||||
"type": "string",
|
||||
"description": "Optional ruleset identifier."
|
||||
}
|
||||
}
|
||||
},
|
||||
"FindingKey": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Unique identifier for a vulnerability finding.",
|
||||
"required": [
|
||||
"componentPurl",
|
||||
"componentVersion",
|
||||
"cveId"
|
||||
],
|
||||
"properties": {
|
||||
"componentPurl": {
|
||||
"type": "string",
|
||||
"description": "Component package URL (purl)."
|
||||
},
|
||||
"componentVersion": {
|
||||
"type": "string",
|
||||
"description": "Component version string."
|
||||
},
|
||||
"cveId": {
|
||||
"type": "string",
|
||||
"description": "Vulnerability identifier (e.g., CVE)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"MaterialChangeType": {
|
||||
"type": "string",
|
||||
"description": "Material change types emitted by Smart-Diff.",
|
||||
"enum": [
|
||||
"reachability_flip",
|
||||
"vex_flip",
|
||||
"range_boundary",
|
||||
"intelligence_flip"
|
||||
]
|
||||
},
|
||||
"VexStatusType": {
|
||||
"type": "string",
|
||||
"description": "VEX status values captured in Smart-Diff risk state.",
|
||||
"enum": [
|
||||
"affected",
|
||||
"not_affected",
|
||||
"fixed",
|
||||
"under_investigation",
|
||||
"unknown"
|
||||
]
|
||||
},
|
||||
"RiskState": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Risk state captured for a finding at a point in time.",
|
||||
"required": [
|
||||
"vexStatus",
|
||||
"kev"
|
||||
],
|
||||
"properties": {
|
||||
"reachable": {
|
||||
"type": "boolean",
|
||||
"description": "Reachability flag (null/absent indicates unknown)."
|
||||
},
|
||||
"vexStatus": {
|
||||
"$ref": "#/$defs/VexStatusType",
|
||||
"description": "VEX status value."
|
||||
},
|
||||
"inAffectedRange": {
|
||||
"type": "boolean",
|
||||
"description": "True if the component version is within the affected range."
|
||||
},
|
||||
"kev": {
|
||||
"type": "boolean",
|
||||
"description": "True if the vulnerability is in the KEV catalog."
|
||||
},
|
||||
"epssScore": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "EPSS score (0..1)."
|
||||
},
|
||||
"policyFlags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 0,
|
||||
"description": "Policy flags contributing to the decision."
|
||||
}
|
||||
}
|
||||
},
|
||||
"MaterialChange": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Single material change detected for a finding.",
|
||||
"required": [
|
||||
"findingKey",
|
||||
"changeType",
|
||||
"reason"
|
||||
],
|
||||
"properties": {
|
||||
"findingKey": {
|
||||
"$ref": "#/$defs/FindingKey",
|
||||
"description": "Finding key for the change."
|
||||
},
|
||||
"changeType": {
|
||||
"$ref": "#/$defs/MaterialChangeType",
|
||||
"description": "Type of material change detected."
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "Human-readable reason for the change."
|
||||
},
|
||||
"previousState": {
|
||||
"$ref": "#/$defs/RiskState",
|
||||
"description": "Previous risk state (when available)."
|
||||
},
|
||||
"currentState": {
|
||||
"$ref": "#/$defs/RiskState",
|
||||
"description": "Current risk state (when available)."
|
||||
},
|
||||
"priorityScore": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Priority score derived from change rules and intelligence."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Json.Schema;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Types.Tests;
|
||||
|
||||
public sealed class SmartDiffSchemaValidationTests
|
||||
{
|
||||
[Fact]
|
||||
public void SmartDiffSchema_ValidatesSamplePredicate()
|
||||
{
|
||||
var schemaPath = Path.Combine(AppContext.BaseDirectory, "schemas", "stellaops-smart-diff.v1.schema.json");
|
||||
File.Exists(schemaPath).Should().BeTrue($"schema file should be copied to '{schemaPath}'");
|
||||
|
||||
var schema = JsonSchema.FromText(File.ReadAllText(schemaPath));
|
||||
using var doc = JsonDocument.Parse("""
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"baseImage": {
|
||||
"name": "example/base",
|
||||
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"tag": "1.0"
|
||||
},
|
||||
"targetImage": {
|
||||
"name": "example/target",
|
||||
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"tag": "2.0"
|
||||
},
|
||||
"diff": {
|
||||
"filesAdded": ["./a.txt"],
|
||||
"packagesChanged": [
|
||||
{
|
||||
"name": "openssl",
|
||||
"purl": "pkg:deb/openssl@3.0.14",
|
||||
"from": "1.1.1u",
|
||||
"to": "3.0.14"
|
||||
}
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"entrypoint": ["/app/start"],
|
||||
"env": {
|
||||
"FEATURE_X": "true"
|
||||
},
|
||||
"user": {
|
||||
"uid": 1001,
|
||||
"caps": ["NET_BIND_SERVICE"]
|
||||
}
|
||||
},
|
||||
"reachabilityGate": {
|
||||
"reachable": true,
|
||||
"configActivated": true,
|
||||
"runningUser": false,
|
||||
"class": 6,
|
||||
"rationale": "sample"
|
||||
},
|
||||
"scanner": {
|
||||
"name": "StellaOps.Scanner",
|
||||
"version": "2025.12.0",
|
||||
"ruleset": "reachability-2025.12"
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var result = schema.Evaluate(doc.RootElement, new EvaluationOptions
|
||||
{
|
||||
OutputFormat = OutputFormat.List,
|
||||
RequireFormatValidation = true
|
||||
});
|
||||
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SmartDiffSchema_RejectsInvalidReachabilityClass()
|
||||
{
|
||||
var schemaPath = Path.Combine(AppContext.BaseDirectory, "schemas", "stellaops-smart-diff.v1.schema.json");
|
||||
var schema = JsonSchema.FromText(File.ReadAllText(schemaPath));
|
||||
using var doc = JsonDocument.Parse("""
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"baseImage": { "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" },
|
||||
"targetImage": { "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" },
|
||||
"diff": { },
|
||||
"reachabilityGate": { "class": 99 },
|
||||
"scanner": { "name": "StellaOps.Scanner", "version": "2025.12.0" }
|
||||
}
|
||||
""");
|
||||
|
||||
var result = schema.Evaluate(doc.RootElement, new EvaluationOptions
|
||||
{
|
||||
OutputFormat = OutputFormat.List,
|
||||
RequireFormatValidation = true
|
||||
});
|
||||
|
||||
result.IsValid.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user