ecluse:ecluse-core
Safe HaskellNone
LanguageGHC2021

Ecluse.Core.Rules.Types

Description

Data types for the policy rules engine.

The evaluation model lives in Ecluse.Core.Rules; this module holds only the dependency-light types it operates on -- the closed built-in rule vocabulary config selects from, a rule's per-version result, and the overall decision.

A Rule is evaluation-agnostic data: it says what a rule is, never how it is evaluated. How a rule decides is a separate concern that lives in Ecluse.Core.Rules (evalRule dispatches over this data; the engine wraps it in a PreparedRule to run it).

Synopsis

The built-in rule vocabulary

data Rule Source #

The closed, evaluation-agnostic vocabulary of built-in rules an operator selects and refines in config. Most built-in rules reason only over the PackageDetails an adapter already fetched; AllowIfRemediatesCve and DenyIfCve additionally consult the local advisory database through the boot-bound RuleDeps.

This is data, not the engine's runtime representation: a small, inspectable, Eq/Show enum so config can parse, patch (override a rule's parameters), and name each rule. Ecluse.Core.Rules turns it into the engine's runtime PreparedRule (binding how it is evaluated) for evaluation. It carries no allow/deny "direction" -- whether a rule admits or blocks is simply what its evaluation returns.

It is also the security boundary on what config can express: untrusted config only ever yields closed Rule data, never arbitrary computation. A rule whose evaluation performs IO (AllowIfRemediatesCve, DenyIfCve) is a plain constructor here that evalRule dispatches on; arbitrary evaluation closures are a code-layer capability, never reachable from config.

Constructors

AllowScope Scope

Unconditionally allow every package under the given scope.

AllowIfOlderThan NominalDiffTime

Allow a version only if it was published at least this long ago. Guards against race-to-publish supply-chain attacks where an attacker publishes a malicious version and hopes it is consumed before takedown.

DenyInstallTimeExecution

Deny any package version that runs code at install time -- npm install scripts, a RubyGems native-extension build, a PyPI sdist build backend -- a common arbitrary-code-execution vector. Yields no decision otherwise.

DenyByIdentity Text

A hard deny for a specific package or package@version. Evaluated at top precedence (above AllowScope) as a post-mirror revocation mechanism.

AllowByIdentity Text

Allow a specific package or package@version by exact identity -- the allow twin of DenyByIdentity and the operator's explicit escape hatch, e.g. for a security fix published under a version string the remediation fast lane's exact-match probe cannot see. Top of the allow band, still under every deny default.

AllowIfRemediatesCve

Fast-track a version a synced advisory names as its exact fix, so a security patch is admitted immediately rather than waiting out the publish-age quarantine. Effectful: it consults the local advisory database (CveLookup) through the boot-bound RuleDeps, and abstains when no database is loaded, when the version is not an exact fixed bound, or when the version still sits inside another advisory's affected range.

DenyIfCve DenyIfCveParams

Deny a version a synced advisory records as affected, at or above the configured severity threshold -- the deny direction over the same advisory database as AllowIfRemediatesCve, with the deliberately __opposite failure mode__: where an unconfirmable remediation merely falls back to the quarantine, an unanswerable deny check refuses the version (unless the operator configured it fail-open; see DenyIfCveParams). Ships opt-in, not in the default policy: enabled before the mirror is warmed, it would deny the historical versions an existing build already depends on.

Instances

Instances details
Show Rule Source # 
Instance details

Defined in Ecluse.Core.Rules.Types

Methods

showsPrec :: Int -> Rule -> ShowS #

show :: Rule -> String #

showList :: [Rule] -> ShowS #

Eq Rule Source # 
Instance details

Defined in Ecluse.Core.Rules.Types

Methods

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

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

data DenyIfCveParams Source #

DenyIfCve's configured behaviour -- a separate record rather than fields on the constructor, so its selectors stay total under the sum (-Wpartial-fields).

Constructors

DenyIfCveParams 

Fields

  • dicMinSeverity :: Double

    The CVSS base score (0 to 10) at or above which an affecting advisory denies; below it the advisory is noted in the audit trail but does not block. A qualitative label counts as its band's ceiling, and an unscored advisory counts as above every threshold (severityAtLeast): severity that cannot be proven low must not slip a deny gate.

  • dicOnUnavailable :: FailureAlignment

    How the rule resolves when the advisory database cannot answer (not loaded, failing, or timed out): FailDeny refuses the version (fail-closed, the shipped default), FailNoDecision skips the rule (fail-open, for the operator whose availability outranks a blind gate; the skip is recorded in the decision's audit reasons).

ruleName :: Rule -> Text Source #

A stable, human-facing name for a rule -- its identity, derived from the data: the boot-order tiebreak and the credited identity in logs and denial messages.

Precedence

data PrecededRule Source #

A Rule paired with the integer precedence at which it competes (higher wins). This is config's resolved-policy element; Ecluse.Core.Rules prepares it into the engine's runtime rule, whose boot-time ordering (bootOrder) turns precedence -- and, at equal precedence, the rule name -- into the single total order the engine walks.

Precedence is a field, not an Ord instance: equal precedence between two rules is legal (it is resolved by name in the boot order), so a total derived Ord would be non-antisymmetric -- unlawful and misleading. This mirrors Version, whose ordering likewise goes through a function rather than a derived instance.

Constructors

PrecededRule 

Fields

Instances

Instances details
Show PrecededRule Source # 
Instance details

Defined in Ecluse.Core.Rules.Types

Eq PrecededRule Source # 
Instance details

Defined in Ecluse.Core.Rules.Types

defaultPrecedence :: Rule -> Int Source #

The default precedence for a rule type -- used when a policy omits an explicit precedence for a rule.

The rule types climb one ladder, most-passive to most-decisive:

AllowIfOlderThan (100) < AllowIfRemediatesCve (150) < AllowScope (200) < DenyIfCve (225) < AllowByIdentity (250) < DenyInstallTimeExecution (300) < DenyByIdentity (400)@

Two placements carry the design and are worth stating plainly:

  • __DenyByIdentity and DenyInstallTimeExecution default strictly above every allow__, so a blanket "any deny overrides any allow" holds for them out of the box: revocation and the install-script deny keep the last word.
  • DenyIfCve is the deliberate exception: it sits below AllowByIdentity so an operator's exact-identity allow -- the explicit "I have decided this specific version must ship" escape hatch -- overrides an advisory deny (a graceful pin for a false positive or an accepted risk), while still sitting above the passive age gate, the remediation lane, and a scope allow-list, so an unpinned affected version is denied despite them.

An operator may still raise a specific allow above a specific deny (or vice versa) with an explicit precedence -- the per-type defaults set only the out-of-the-box ordering.

defaultAllowIfOlderThanPrecedence :: Int Source #

Default precedence of AllowIfOlderThan: the lowest band, a passive quarantine that yields to an explicit allow-list and to every deny.

defaultAllowIfRemediatesCvePrecedence :: Int Source #

Default precedence of AllowIfRemediatesCve: above the passive age quarantine, which is the point of the fast lane -- a security fix is admitted immediately instead of waiting out min-age -- but below AllowScope, so a scoped package an operator already trusts never pays the advisory probe and the more explicit rule keeps the audit credit.

defaultAllowScopePrecedence :: Int Source #

Default precedence of AllowScope: above the passive age quarantine -- an explicit allow-list of a trusted internal scope is a stronger statement than the time gate -- but still below every deny.

defaultDenyIfCvePrecedence :: Int Source #

Default precedence of DenyIfCve: above the passive age gate, the remediation lane, and a scope allow-list, so an affected version is denied despite them -- but deliberately below AllowByIdentity, so an operator's exact-identity allow can pin a specific version past a false-positive or risk-accepted advisory. The one deny type that is not strictly above every allow (see defaultPrecedence).

defaultAllowByIdentityPrecedence :: Int Source #

Default precedence of AllowByIdentity: the top of the allow band -- an exact identity is the most explicit allow an operator can state. It sits above DenyIfCve (the identity pin overrides an advisory deny) but still strictly below the DenyInstallTimeExecution and DenyByIdentity defaults, so the install-script deny and revocation keep the last word.

defaultDenyInstallTimeExecutionPrecedence :: Int Source #

Default precedence of DenyInstallTimeExecution: the deny band, strictly above every allow default, so a matching deny overrides any allow out of the box.

defaultDenyByIdentityPrecedence :: Int Source #

Default precedence of DenyByIdentity: the top precedence, strictly above every other rule (including explicit allow-lists), to serve as a hard revocation.

Evaluation

data EvalContext Source #

Ambient information a rule may need that is not part of the package itself: the wall-clock "now" for age calculations, and the active advisory database's identity for a decision's audit trail.

Constructors

EvalContext 

Fields

  • ctxNow :: UTCTime

    The wall-clock "now" for age-based rules.

  • ctxAdvisoryEtag :: Maybe DbEtag

    The advisory database DbEtag active when this request was admitted, or Nothing when none is loaded (or on a path that does not consult one). It is the artifact a denial's audit line names as active at emit; it is deliberately not "the database this decision was evaluated against", since a shadow-swap may land mid-request. Resolved once per request.

Instances

Instances details
Show EvalContext Source # 
Instance details

Defined in Ecluse.Core.Rules.Types

Eq EvalContext Source # 
Instance details

Defined in Ecluse.Core.Rules.Types

mkEvalContext :: IO UTCTime -> IO (Maybe DbEtag) -> IO EvalContext Source #

Assemble the ambient evaluation context -- the one assembly point for every consumer (the packument sweep, the tarball gate, and the mirror worker's ingest re-evaluation), so what feeds a decision is defined once, not at each call site.

The contract the single point holds: ctxNow must come from the injected clock the mount's decisions share (pdNow, which the worker's bundle reuses), never an ad-hoc getCurrentTime, so the age gate cannot drift between contexts; ctxAdvisoryEtag is audit-only (it never enters a rule's decision), so a consumer that emits no audit line passes Nothing without changing any decision.

type Reason = Text Source #

A human-facing reason a rule attaches to its result, kept for the audit trail.

data RuleVerdict Source #

What a single rule returns for a single package version: a deterministic verdict. The rule computes its answer -- over the package, and for the effectful rules the advisory database -- and returns one of these. A rule cannot manufacture an Unavailable; that is the distinction the resilience harness turns on. A verdict is a decided value the harness takes at face value, never a fault it retries.

A verdict is decisive iff it is Allow, Deny, or CannotVet FailDeny _. NoDecision and CannotVet FailNoDecision _ are non-decisive no-ops; the engine collects their reasons (in boot order) for the deny-by-default audit trail.

Constructors

Allow Reason

This rule admits the package (with a human reason). Decisive.

Deny Reason

This rule blocks the package (with a human reason). Decisive.

NoDecision Reason

This rule has no opinion; the reason is kept for the audit trail. A no-op.

CannotVet FailureAlignment Reason

The rule reached the package but cannot vet it -- a __deterministic, in-process absence__, not a fault (today: no advisory database is loaded). It carries its own failure alignment: a FailDeny rule is decisive (fail-closed, → Undecidable), a FailNoDecision rule is a no-op (fail-open). It carries no Transience on purpose: the absence is deterministic, so no in-process retry can change it -- which is exactly why the harness must not route it through the retry/breaker path.

Instances

Instances details
Show RuleVerdict Source # 
Instance details

Defined in Ecluse.Core.Rules.Types

Eq RuleVerdict Source # 
Instance details

Defined in Ecluse.Core.Rules.Types

data RuleEvaluation Source #

The outcome the resilience harness produces for one rule: either the rule Decided (any RuleVerdict, taken at face value), or the harness could not obtain a verdict at all and the evaluation is Unavailable -- the rule's IO threw, timed out, or its source circuit breaker was open. Only the harness constructs Unavailable; a rule cannot, so the retry/breaker machinery provably reacts only to a fault the harness itself observed, never to a verdict a rule deliberately returned.

Decisive iff it credits a Decision: a decisive RuleVerdict, or an Unavailable _ FailDeny _. A non-decisive verdict, or an Unavailable _ FailNoDecision _, is a no-op whose reason is gathered for the audit trail.

Constructors

Decided RuleVerdict

The rule returned a verdict; the harness takes it at face value.

Unavailable Transience FailureAlignment Reason

The harness could not obtain a verdict: the rule's IO failed, timed out, or its source circuit breaker is open. It carries the rule's failure alignment (a FailDeny evaluation is decisive → Undecidable, a FailNoDecision one is a no-op) and a Transience recording whether a retry can help. Only the harness builds this.

data FailureAlignment Source #

How a rule aligns when it cannot vet a version, or its evaluation faults.

There is deliberately no FailAllow: a failed or uncomputable check must never admit unvetted bytes. A rule whose verdict is load-bearing for safety fails closed (FailDeny); a remediation/allow-direction rule whose missing signal should not block availability fails open (FailNoDecision).

Constructors

FailDeny

Fail closed. An uncomputable result is decisive: the version is not admitted.

FailNoDecision

Fail open. An uncomputable result is a no-op: the rule simply does not fire.

data Decision Source #

The overall decision for a package version against a whole rule set.

The deciding rule is credited by name (Text): a rule's stable identity is its name (see ruleName), independent of how it is evaluated.

Constructors

Admitted Text Reason

Admitted by the named rule, with its reason (was Approved/ApprovedEffectful).

Blocked Text Reason

Blocked by the named rule, with its reason (was Denied/DeniedEffectful).

BlockedByDefault [Reason]

No rule was decisive. Deny-by-default; carries every non-decisive reason, in boot order, so the denial response can explain what was considered.

Undecidable Transience Reason

Undecidable: a FailDeny rule that could not be computed won, so the version could not be vetted. Fail-closed -- it is not admitted (a packument filters it out like a denial; a concrete artifact surfaces a 503/500 by the serve error model). The Transience carries whether a retry can help; the Reason is the audit reason.

Instances

Instances details
Show Decision Source # 
Instance details

Defined in Ecluse.Core.Rules.Types

Eq Decision Source # 
Instance details

Defined in Ecluse.Core.Rules.Types

Unavailability

data Transience Source #

Whether an unavailability is expected to resolve on its own.

This is the single distinction the serve status mapping turns on: a transient cause (WillResolve) is worth retrying (a 503); a permanent or internal one (WontResolve) is not, so it must not be dressed up as a retryable 503 (it is a 500). The resilience harness sets it from the nature of the failure: an upstream outage, rate limit, timeout, or open breaker is transient; an internal or parse fault is not.

Constructors

WillResolve (Maybe RetryAfter)

Transient -- a retry may succeed (an advisory source briefly down, a timeout, an open circuit breaker). The optional RetryAfter is the delay to suggest to the client.

WontResolve

Not expected to self-heal (an internal or parse error). Retrying cannot help, so the request is a 500, never a 503.

Instances

Instances details
Show Transience Source # 
Instance details

Defined in Ecluse.Core.Rules.Types

Eq Transience Source # 
Instance details

Defined in Ecluse.Core.Rules.Types

newtype RetryAfter Source #

A Retry-After delay, in whole seconds. A 'newtype' so a raw count of seconds is never confused with some other integer when it reaches the response header.

Constructors

RetryAfter Int