| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
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
- data Rule
- data DenyIfCveParams = DenyIfCveParams {}
- ruleName :: Rule -> Text
- data PrecededRule = PrecededRule {
- rulePrecedence :: Int
- prRule :: Rule
- defaultPrecedence :: Rule -> Int
- defaultAllowIfOlderThanPrecedence :: Int
- defaultAllowIfRemediatesCvePrecedence :: Int
- defaultAllowScopePrecedence :: Int
- defaultDenyIfCvePrecedence :: Int
- defaultAllowByIdentityPrecedence :: Int
- defaultDenyInstallTimeExecutionPrecedence :: Int
- defaultDenyByIdentityPrecedence :: Int
- data EvalContext = EvalContext {}
- mkEvalContext :: IO UTCTime -> IO (Maybe DbEtag) -> IO EvalContext
- type Reason = Text
- data RuleVerdict
- data RuleEvaluation
- data FailureAlignment
- data Decision
- data Transience
- newtype RetryAfter = RetryAfter Int
The built-in rule vocabulary
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 |
| 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
( |
| 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 |
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
| |
Instances
| Show DenyIfCveParams Source # | |
Defined in Ecluse.Core.Rules.Types Methods showsPrec :: Int -> DenyIfCveParams -> ShowS # show :: DenyIfCveParams -> String # showList :: [DenyIfCveParams] -> ShowS # | |
| Eq DenyIfCveParams Source # | |
Defined in Ecluse.Core.Rules.Types Methods (==) :: DenyIfCveParams -> DenyIfCveParams -> Bool # (/=) :: DenyIfCveParams -> DenyIfCveParams -> Bool # | |
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
| Show PrecededRule Source # | |
Defined in Ecluse.Core.Rules.Types Methods showsPrec :: Int -> PrecededRule -> ShowS # show :: PrecededRule -> String # showList :: [PrecededRule] -> ShowS # | |
| Eq PrecededRule Source # | |
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:
- __
DenyByIdentityandDenyInstallTimeExecutiondefault 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. DenyIfCveis the deliberate exception: it sits belowAllowByIdentityso 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
| |
Instances
| Show EvalContext Source # | |
Defined in Ecluse.Core.Rules.Types Methods showsPrec :: Int -> EvalContext -> ShowS # show :: EvalContext -> String # showList :: [EvalContext] -> ShowS # | |
| Eq EvalContext Source # | |
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.
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 are non-decisive no-ops; the
engine collects their reasons (in boot order) for the deny-by-default audit trail.CannotVet FailNoDecision _
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 |
Instances
| Show RuleVerdict Source # | |
Defined in Ecluse.Core.Rules.Types Methods showsPrec :: Int -> RuleVerdict -> ShowS # show :: RuleVerdict -> String # showList :: [RuleVerdict] -> ShowS # | |
| Eq RuleVerdict Source # | |
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
. A non-decisive verdict, or an Unavailable _ FailDeny _, is a no-op whose reason is gathered for the audit trail.Unavailable _
FailNoDecision _
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 |
Instances
| Show RuleEvaluation Source # | |
Defined in Ecluse.Core.Rules.Types Methods showsPrec :: Int -> RuleEvaluation -> ShowS # show :: RuleEvaluation -> String # showList :: [RuleEvaluation] -> ShowS # | |
| Eq RuleEvaluation Source # | |
Defined in Ecluse.Core.Rules.Types Methods (==) :: RuleEvaluation -> RuleEvaluation -> Bool # (/=) :: RuleEvaluation -> RuleEvaluation -> Bool # | |
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. |
Instances
| Show FailureAlignment Source # | |
Defined in Ecluse.Core.Rules.Types Methods showsPrec :: Int -> FailureAlignment -> ShowS # show :: FailureAlignment -> String # showList :: [FailureAlignment] -> ShowS # | |
| Eq FailureAlignment Source # | |
Defined in Ecluse.Core.Rules.Types Methods (==) :: FailureAlignment -> FailureAlignment -> Bool # (/=) :: FailureAlignment -> FailureAlignment -> Bool # | |
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 |
| Blocked Text Reason | Blocked by the named rule, with its reason (was |
| 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 |
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 |
| WontResolve | Not expected to self-heal (an internal or parse error). Retrying cannot
help, so the request is a |
Instances
| Show Transience Source # | |
Defined in Ecluse.Core.Rules.Types Methods showsPrec :: Int -> Transience -> ShowS # show :: Transience -> String # showList :: [Transience] -> ShowS # | |
| Eq Transience Source # | |
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 |
Instances
| Show RetryAfter Source # | |
Defined in Ecluse.Core.Rules.Types Methods showsPrec :: Int -> RetryAfter -> ShowS # show :: RetryAfter -> String # showList :: [RetryAfter] -> ShowS # | |
| Eq RetryAfter Source # | |
Defined in Ecluse.Core.Rules.Types | |
| Ord RetryAfter Source # | |
Defined in Ecluse.Core.Rules.Types Methods compare :: RetryAfter -> RetryAfter -> Ordering # (<) :: RetryAfter -> RetryAfter -> Bool # (<=) :: RetryAfter -> RetryAfter -> Bool # (>) :: RetryAfter -> RetryAfter -> Bool # (>=) :: RetryAfter -> RetryAfter -> Bool # max :: RetryAfter -> RetryAfter -> RetryAfter # min :: RetryAfter -> RetryAfter -> RetryAfter # | |