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 additionally consults 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) 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. The one effectful built-in: 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.

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 #

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.

Every deny type defaults strictly above every allow type, so "any deny overrides any allow" holds out of the box. The rule types occupy two bands: the allow band, ordered by how explicit the operator's statement is (AllowIfOlderThan < AllowIfRemediatesCve < AllowScope < AllowByIdentity), then the deny band (defaultDenyInstallTimeExecutionPrecedence, defaultDenyByIdentityPrecedence) strictly above all of it. An operator may still raise a specific allow above a specific deny by giving it a higher 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.

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 -- yet still strictly below every deny default, so revocation and the install-script deny 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

newtype EvalContext Source #

Ambient information a rule may need that is not part of the package itself (the wall-clock "now" for age calculations).

Constructors

EvalContext 

Fields

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

type Reason = Text Source #

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

data RuleResult Source #

The verdict of a single rule against a single package version.

A result is decisive iff it is Allow, Deny, or Unavailable _ FailDeny _. NoDecision and Unavailable _ 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.

Unavailable Transience FailureAlignment Reason

The rule could not be computed -- its IO failed, timed out, or its source circuit breaker is open. It carries its own failure alignment: a FailDeny rule is decisive (fail-closed, → Undecidable), a FailNoDecision rule is a no-op (fail-open). The Transience records whether a retry can help; the Reason is the audit reason. The built-in rules never yield this.

Instances

Instances details
Show RuleResult Source # 
Instance details

Defined in Ecluse.Core.Rules.Types

Eq RuleResult Source # 
Instance details

Defined in Ecluse.Core.Rules.Types

data FailureAlignment Source #

How a rule's Unavailable result aligns when the rule could not be computed.

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