ecluse:ecluse-core
Safe HaskellNone
LanguageGHC2021

Ecluse.Core.Cve

Description

The advisory lookup capability: answer CVE questions about a package version from a local, already-synced osv.db artifact, never the network.

The handle is deliberately dumb data access over one artifact file. Rule semantics live in pure predicates over what it returns (insideAffectedRange), because SQLite's text collation cannot order versions; only compareVersions can. The one deliberate exception is cveRemediationProbe: a fixed bound in the artifact is a single canonical version string, so exact-fix matching is plain string equality and rides the (package_name, fixed_version) index in one traversal. A fix published under a non-canonical version string misses the probe and simply waits out the ordinary quarantine; the operator workaround is an explicit AllowByIdentity rule.

An artifact is accepted or rejected at openCveDb (epoch stamp, integrity, strict-schema conformance, ecosystem), with rejection as a value: the caller keeps its last known-good handle and alarms. See Ecluse.Core.Cve.Internal for the hardening detail.

Ownership is split at the type level: openCveDb yields a CveDb, the owning resource whose holder alone may cveDbClose; consumers are handed only its CveLookup view, so nothing evaluating rules can release a shared connection. The owner (the background sync's shadow-swap, which retires an artifact only when no evaluation still reads it) holds the CveDb and closes it explicitly.

Synopsis

The owning resource

data CveDb Source #

One opened artifact: the consumer view plus the owner's close. Whoever holds this owns the connection's lifetime; hand consumers cveDbLookup only.

Constructors

CveDb 

Fields

  • cveDbLookup :: CveLookup

    The view consumers query through.

  • cveDbClose :: IO ()

    Release the artifact's connection. Owner-only; the artifact must no longer be read through this handle's view afterwards. Never throws: a close fault is absorbed inside the handle, since the connection is being discarded either way and every close site (a swap-out drain, an exception unwind) wants the same disposition.

  • cveDbMeta :: [(Text, Text)]

    The artifact's meta provenance rows (Pilot version, ecosystem, build timestamp, source URL, row count), snapshotted at open, key-sorted. The audit surface that ties this handle's decisions to the exact database that produced them.

openCveDb :: Ecosystem -> FilePath -> IO (Either CveDbRejected CveDb) Source #

Open an osv.db artifact and build the owning handle over it, or reject the artifact (CveDbRejected) with its connection already closed. Nothing an artifact carries can make this throw: acceptance admits only a conformant STRICT schema whose stored values the integrity walk verified, so the provenance decode below it is total. Faults below the artifact contract (an unopenable file) still throw, and then too the connection is already closed: an exception never leaks it.

The consumer view

data CveLookup Source #

Advisory questions about one ecosystem's artifact -- the read-only view a consumer (a rule evaluation) is handed. It deliberately cannot release the underlying connection; that is the owning CveDb's capability.

Names and versions are the artifact's own vocabulary: the OSV wire package name (scope inline, e.g. @scope/name) and verbatim version text. Callers render their domain values to that form at the boundary.

Constructors

CveLookup 

Fields

What a lookup returns

data AdvisoryRange Source #

One advisory segment recorded against a package: the advisory's identifier, its CVSS base score (0 to 10, Nothing when unscored), and the affected interval's bounds as the artifact stores them (verbatim version text). The lower bound arIntroduced is inclusive (Nothing == from the beginning); the upper bound is arFixed (exclusive) or arLastAffected (inclusive) or neither (open-ended). An exactly-enumerated affected version is a point segment (introduced == last_affected).

Instances

Instances details
Show AdvisoryRange Source # 
Instance details

Defined in Ecluse.Core.Cve.Internal

Eq AdvisoryRange Source # 
Instance details

Defined in Ecluse.Core.Cve.Internal

Rejection

data CveDbRejected Source #

Why a downloaded artifact was refused before a handle was built over it.

A rejection is a value, not an exception: the caller (the sync task, once it exists) has a real decision to make, keep the last known-good database and alarm, rather than a fault to unwind from.

Constructors

CveDbWrongEpoch Int

The artifact's user_version stamp (carried) does not match this binary's osvSchemaEpoch.

CveDbIntegrityFailed [Text]

The artifact is not a usable SQLite database: either it is not a database at all (absent or wrong header magic, which SQLite reports as SQLITE_NOTADB on the first header read), or PRAGMA quick_check found it structurally corrupt (a malformed, truncated, or crafted b-tree). The carried lines are the thrown error or the integrity report (which SQLite caps at 100 problems).

CveDbSchemaNonConformant Text

A required relation (carried) does not conform to the epoch's schema contract: absent, not a real STRICT table, or missing a required column with its declared type. A view here is attacker-authored SQL wearing the table's name; a lax (non-STRICT) table would leave the reader's decodes exposed to type-confused values.

CveDbEcosystemMismatch (Maybe Text)

The artifact's meta table names a different ecosystem (carried) than the one this handle was asked to serve, or carries no ecosystem row at all so the ecosystem cannot be confirmed (Nothing). An absent meta table is caught earlier, as CveDbSchemaNonConformant.

Instances

Instances details
Show CveDbRejected Source # 
Instance details

Defined in Ecluse.Core.Cve.Internal

Eq CveDbRejected Source # 
Instance details

Defined in Ecluse.Core.Cve.Internal

Query faults

data CveQueryFault Source #

A query the accepted advisory database could not answer: the SQLite edge threw mid-query (an I/O error on the database file, a connection released out from under a straggling reader). Carries which handle field was asked and the rendered SQLError for the log line; the artifact's content can never produce this (openCveDb acceptance made the row decodes total), so it marks an infrastructural fault, not a data fault.

A confined typed exception, the same shape as CredentialError at the breaker leaf: it is thrown at the SQLite edge inside the handle and absorbed by the one boundary every advisory query runs under -- the rules engine's resilience harness (runEffectfulRule), which resolves it to an Unavailable evaluation and advances the rule's circuit breaker. It never crosses that boundary, so no caller above the rules engine sees it. The alternative (an Either on every CveLookup field) would reshape every rule's evaluation type for a fault only the harness ever handles.

Constructors

CveQueryFault 

Fields

Artifact identity

newtype DbEtag Source #

An artifact version marker: S3's ETag, opaque text compared for equality only. Two objects with equal ETags carry equal bytes, so an unchanged ETag is "nothing to do", a rejected artifact's remembered ETag is "still the same bad artifact", and it names the exact advisory database a rule decision was recorded against.

Constructors

DbEtag Text 

Instances

Instances details
Show DbEtag Source # 
Instance details

Defined in Ecluse.Core.Cve

Eq DbEtag Source # 
Instance details

Defined in Ecluse.Core.Cve

Methods

(==) :: DbEtag -> DbEtag -> Bool #

(/=) :: DbEtag -> DbEtag -> Bool #

Pure range matching

insideAffectedRange :: Ecosystem -> Text -> AdvisoryRange -> Bool Source #

Is this version inside the advisory segment's affected interval, under the ecosystem's version ordering? The interval is introduced <= v bounded above by either v < fixed (exclusive) or v <= last_affected (inclusive), whichever the segment carries, or unbounded when it carries neither. A point segment (introduced == last_affected) is affected only at that exact version.

Fail-closed. Both the remediation fast lane (a fix must not fast-track while it sits inside another advisory's affected range) and the deny gate (an unvettable version must not be admitted) want the same polarity, so every unprovable comparison, an unparseable bound or version, counts as inside: trust is only ever granted on evidence.

severityAtLeast :: Double -> Maybe Double -> Bool Source #

Does this advisory segment's severity meet or exceed the threshold (a CVSS base score, 0 to 10)? arSeverity is the artifact's normalised numeric score (Pilot reduces a CVSS vector or a qualitative label to a number at ingest), or absent.

Fail-closed for the deny direction. The threshold gates a deny, so a severity that cannot be shown to fall below it counts as meeting it: an unscored advisory (Nothing -- most of the npm malware feed) returns True. Only a score strictly below the threshold returns False.