| 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
- ruleName :: Rule -> Text
- data PrecededRule = PrecededRule {
- rulePrecedence :: Int
- prRule :: Rule
- defaultPrecedence :: Rule -> Int
- atDefaultPrecedence :: Rule -> PrecededRule
- defaultAllowIfOlderThanPrecedence :: Int
- defaultAllowIfRemediatesCvePrecedence :: Int
- defaultAllowScopePrecedence :: Int
- defaultAllowByIdentityPrecedence :: Int
- defaultDenyInstallTimeExecutionPrecedence :: Int
- defaultDenyByIdentityPrecedence :: Int
- newtype EvalContext = EvalContext {}
- type Reason = Text
- data RuleResult
- 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
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 |
| 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 ( |
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.
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.
atDefaultPrecedence :: Rule -> PrecededRule Source #
Pair a rule with its type's defaultPrecedence.
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 | |
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 | |
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 are non-decisive no-ops; the
engine collects their reasons (in boot order) for the deny-by-default audit trail.Unavailable _ 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. |
| 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
|
Instances
| Show RuleResult Source # | |
Defined in Ecluse.Core.Rules.Types Methods showsPrec :: Int -> RuleResult -> ShowS # show :: RuleResult -> String # showList :: [RuleResult] -> ShowS # | |
| Eq RuleResult Source # | |
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. |
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 # | |