Documentation
¶
Overview ¶
Package fides talks to a Fides server — the compliance system Hecate records promotions in and asks for permission.
Only the endpoints a promotion needs are here. Fides has a large API; Hecate uses three parts of it:
- verify a trail's attestation chain, so the tamper-evidence claim is checkable by whoever relies on it rather than asserted in documentation;
- check an environment's policy against a trail;
- check whether an artifact is on an environment's allowlist.
The last two are why a Gate has to name a Fides environment at all.
Index ¶
- Constants
- func IsAuth(err error) bool
- func IsNotFound(err error) bool
- func IsUncounted(err error) bool
- type Anchor
- type Approval
- type Artifact
- type Attestation
- type Chain
- type ChangeRequest
- type ChangeVerdict
- type Client
- func (c *Client) Allowlisted(ctx context.Context, environment, sha256 string) (bool, error)
- func (c *Client) Assert(ctx context.Context, sha256, policy string) (*Compliance, error)
- func (c *Client) Attest(ctx context.Context, trail string, a Attestation) error
- func (c *Client) ChangeGate(ctx context.Context, trail string) (*ChangeVerdict, error)
- func (c *Client) ChangeRequestFor(ctx context.Context, trail string) (*ChangeRequest, error)
- func (c *Client) PolicyCheck(ctx context.Context, environment, trail string) (*PolicyVerdict, error)
- func (c *Client) RecordApproval(ctx context.Context, trail string, a Approval) error
- func (c *Client) ReportArtifact(ctx context.Context, a Artifact) error
- func (c *Client) TrailForArtifact(ctx context.Context, sha256 string) (string, error)
- func (c *Client) VerifyChain(ctx context.Context, trail string) (*Chain, error)
- type Compliance
- type Config
- type Control
- type Error
- type PolicyResult
- type PolicyVerdict
- type SegregationOfDuties
- type UncountedError
Constants ¶
const ( // RoleApprover is a reviewer's sign-off. RoleApprover = "approver" // RoleDeployer is the identity that triggers the deployment. Distinct from // the approver on purpose: Fides refuses a trail where they are the same // person, which is the whole point of four-eyes. RoleDeployer = "deployer" )
Approval roles, as Fides names them when it evaluates segregation of duties.
Variables ¶
This section is empty.
Functions ¶
func IsNotFound ¶
IsNotFound reports whether Fides says the thing is not there.
func IsUncounted ¶ added in v0.4.0
IsUncounted reports whether an approval was stored but will never be counted.
Types ¶
type Anchor ¶
type Anchor struct {
Anchored bool `json:"anchored"`
// HeadMatches is false when the chain has moved on since it was anchored,
// or when it was rewritten under the anchor.
HeadMatches bool `json:"head_matches"`
TSAURL string `json:"tsa_url,omitempty"`
AnchoredAt time.Time `json:"anchored_at,omitempty"`
}
Anchor is the external timestamp over a chain head.
type Approval ¶
type Approval struct {
// By is the human the approval belongs to. Required.
//
// Sent as `on_behalf_of`, because Hecate authenticates to Fides with a
// service token: without it every approval Hecate records would carry the
// service account's identity, every identity would be equal, and
// segregation of duties would evaluate one person having done everything.
// Fides honours the delegation only when it is configured to and the email
// is a known user in the organisation — so this can be refused, and a
// refusal is a real answer rather than a transport failure.
By string
// Role is RoleApprover or RoleDeployer.
Role string
// Reason is free text stored with the approval.
Reason string
}
Approval is one identity signing off on a trail in one role.
type Artifact ¶
type Artifact struct {
// SHA256 identifies it. The `sha256:` prefix is stripped before sending —
// Fides keys artifacts on lowercase hex.
SHA256 string
// Trail links the artifact to the evidence recorded against it.
Trail string
// Name is human-readable, e.g. the image repository.
Name string
// Type is what kind of thing it is, e.g. container-image.
Type string
// Tags are arbitrary labels.
Tags map[string]string
}
Artifact is a thing Fides can hold evidence about.
type Attestation ¶
type Attestation struct {
// Name is what this evidence is, e.g. "promotion".
Name string
// Type is the attestation type an environment policy asks for by name, so
// `policy check` can require it. Getting this wrong means a policy that
// requires "deployment" never sees one.
Type string
// ArtifactSHA256 ties the evidence to what was deployed.
ArtifactSHA256 string
// Payload is the evidence body, stored and hashed into the chain.
Payload any
// SignedBy is who or what produced it.
SignedBy string
}
Attestation is one piece of evidence on a trail.
type Chain ¶
type Chain struct {
// Valid is false when the chain has been tampered with, reordered, or had
// an entry deleted.
Valid bool `json:"valid"`
// Count is how many attestations are on the trail.
Count int `json:"count"`
// BrokenAt is the index of the first bad entry, or -1 when valid.
BrokenAt int `json:"broken_at"`
// Reason says what was wrong, when something was.
Reason string `json:"reason,omitempty"`
// ExternalAnchor reports whether the chain head was timestamped by an
// external authority. A valid chain that only we vouch for is a weaker
// claim than one an RFC3161 authority saw at a point in time.
ExternalAnchor *Anchor `json:"external_anchor,omitempty"`
}
Chain is the verdict on a trail's attestation chain.
Fields mirror Fides' own response rather than being renamed, so an operator comparing `hecate verify` with `fides verify-chain` sees the same words.
type ChangeRequest ¶ added in v0.8.0
type ChangeRequest struct {
// Number is the CHG record, e.g. CHG0033184.
Number string `json:"number"`
// State is the readable label — new, assess, authorize, scheduled,
// implement, review, closed — normalised by Fides from ServiceNow's codes.
State string `json:"state"`
// Approval is ServiceNow's own word: "approved", "requested",
// "not requested", "rejected".
Approval string `json:"approval"`
// Risk is ServiceNow's risk as a string, because that is what it sends.
Risk string `json:"risk"`
// OnHold is why a change that looks approved still is not actionable.
OnHold bool `json:"on_hold"`
// ShortDescription is the ticket's own title.
ShortDescription string `json:"short_description"`
// Found is false when ServiceNow had no such change. Kept because "the
// change does not exist" and "the change is not approved" are different
// answers with different fixes.
Found bool `json:"found"`
// Compliant is whether the attestation satisfied the type's rules — Fides'
// judgement, not a field of the change itself.
Compliant bool `json:"-"`
// At is when it was attested, so a reader can tell a fresh check from one
// made a fortnight ago.
At string `json:"-"`
}
ChangeRequest is the ITSM change this crossing is governed by.
Read from the trail's `servicenow-change` attestation rather than from ServiceNow. That is the deliberate choice: the attestation is what the gate judged, hash-chained into the ledger at the moment of judgement, and a live lookup can disagree with it — a change closed since the crossing would make an audit trail claim a decision nobody made. Hecate also needs no ServiceNow credentials of its own this way, and there is no second client to keep in step with Fides'.
type ChangeVerdict ¶
type ChangeVerdict struct {
// Recommendation is "approve" or "hold".
Recommendation string `json:"recommendation"`
// Approved is Fides' own boolean, true only when every control is satisfied
// and a human has signed off.
Approved bool `json:"approved"`
// RiskScore is 0-100, higher being worse.
RiskScore int `json:"risk_score"`
// RiskLevel is "low", "medium" or "high".
RiskLevel string `json:"risk_level"`
// Passed names the controls that were satisfied, by key alone — Fides sends
// strings here and objects for the two below, and that asymmetry is real.
Passed []string `json:"passed,omitempty"`
// Failed and MissingEvidence name the controls that stopped it, so a held
// crossing can say what would unblock it.
Failed []Control `json:"failed,omitempty"`
MissingEvidence []Control `json:"missing_evidence,omitempty"`
// Waived are the controls a human has excused, with who excused them and
// until when. A waiver is a governed exception rather than a pass, so it is
// reported separately: an auditor's first question about a green gate is
// which of it was waived.
Waived []Control `json:"waived,omitempty"`
// Attestations counts the evidence on the trail.
Attestations struct {
Total int `json:"total"`
NonCompliant int `json:"non_compliant"`
} `json:"attestations"`
// Approvals is who signed off, and whether that satisfied four-eyes.
Approvals struct {
Count int `json:"count"`
HumanApprovers int `json:"human_approvers"`
FourEyes bool `json:"four_eyes"`
Approvers []string `json:"approvers,omitempty"`
Deployers []string `json:"deployers,omitempty"`
} `json:"approvals"`
// SoD is Fides' segregation-of-duties finding: committer, approver and
// deployer must be three distinct people.
SoD *SegregationOfDuties `json:"segregation_of_duties,omitempty"`
// Summary is Fides' own sentence about the verdict.
Summary string `json:"summary,omitempty"`
}
ChangeVerdict is the evidence-backed change-approval decision for a trail.
func (ChangeVerdict) Blockers ¶
func (v ChangeVerdict) Blockers() []string
Blockers lists what is standing in the way, for a message a human can act on.
func (ChangeVerdict) Held ¶
func (v ChangeVerdict) Held() bool
Held reports whether the verdict withholds approval.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a Fides API client.
func (*Client) Allowlisted ¶
Allowlisted reports whether an artifact digest is approved for an environment. The digest is the bare sha256 hex, as Fides stores it.
func (*Client) Assert ¶
Assert checks an artifact digest against a named policy. The policy may be empty, which asks Fides for every policy that applies.
func (*Client) ChangeGate ¶
ChangeGate reads the change-approval verdict for a trail.
func (*Client) ChangeRequestFor ¶ added in v0.8.0
ChangeRequestFor reads the change this trail's crossing is governed by, or nil when no ITSM check has been recorded.
Two calls because the search endpoint answers with metadata and not payloads: one to find the newest servicenow-change attestation, one to read it. The alternative is asking ServiceNow directly, which needs credentials Hecate does not have and answers a different question — see ChangeRequest.
func (*Client) PolicyCheck ¶
func (c *Client) PolicyCheck(ctx context.Context, environment, trail string) (*PolicyVerdict, error)
PolicyCheck evaluates an environment's policy against a trail.
Both identifiers are UUIDs: the environment comes from the Gate, the trail from the crossing being judged.
func (*Client) RecordApproval ¶
RecordApproval records a sign-off on a trail, so Fides can evaluate segregation of duties over the identities involved.
**What the change gate actually checks** is narrower than it looks, and getting this wrong is easy: the verdict is `no failing controls, no missing evidence, and at least one human approver`. It does not compare identities. Pairwise distinctness of committer, approver and deployer is evaluated separately and only produces an attestation of type `segregation-of-duties` — which changes the verdict solely when some control lists that type among the evidence it requires.
So recording both roles matters for the controls that ask for four-eyes, and the *count* of human approvers is what lifts a plain hold.
Returns an UncountedError when Fides stored the approval in a form the gate will not count. That is not a transport failure and retrying will not fix it; see UncountedError for what will.
func (*Client) ReportArtifact ¶
ReportArtifact records an artifact against a trail.
**It refuses to report without a trail, and that is a safety rule rather than validation.** Fides upserts on the digest with `ON CONFLICT (sha256) DO UPDATE SET trail_id = EXCLUDED.trail_id`, so reporting a digest with an empty trail would overwrite the link CI made when it attached the SBOM and the scans — silently detaching exactly the evidence a change gate exists to read. An artifact Hecate cannot link is one it should leave alone.
func (*Client) TrailForArtifact ¶
TrailForArtifact finds the trail an artifact digest was built on.
The trail that matters already exists: CI opened it when it built the image and recorded the SBOM and scan attestations there. Those attestations are exactly what an environment policy and the change gate judge, so a crossing has to gate on *that* trail. A trail Hecate opened fresh would carry none of them, and both checks would refuse every promotion — a gate that always says no is a gate somebody switches off.
Returns "" when Fides has never seen the digest, which is not an error: it means CI did not register the artifact, and the caller decides whether that is disqualifying.
The lookup is one filtered call: /search/artifacts takes the digest and, since fides#442, returns trail_id alongside it.
It falls back to reading /artifacts when the response has no trail_id *key*, which is how a Fides predating that change answers. The distinction matters and is why this checks for the key rather than for a null value: an artifact genuinely built outside a trail also reports trail_id, as null. Treating "absent" and "null" alike would make an old server look like an org whose artifacts have no trails, and TrailForArtifact would answer "" — which the caller reads as "CI never registered this digest" and gates on. A silently wrong answer, rather than a slow one.
The fallback is worth keeping until Hecate states a minimum Fides version, because /artifacts runs a per-row SBOM query and embeds the payload: the response is the size of every SBOM in the organisation, hundreds of megabytes reachable, to learn one 36-byte trail id. Slow is survivable; wrong is not.
type Compliance ¶
type Compliance struct {
Compliant bool `json:"compliant"`
Violations []string `json:"violations,omitempty"`
}
Compliance is the answer to `assert`: does this artifact satisfy a policy?
type Config ¶
type Config struct {
// BaseURL is the server root, e.g. https://fides.acme.io. Required: there
// is no public Fides to default to.
BaseURL string
// Token is a Fides API key, sent as a bearer token.
Token string
// Timeout bounds a single call. Zero uses 30s.
Timeout time.Duration
}
Config is what a client needs to reach a Fides server.
type Control ¶
type Control struct {
Key string `json:"control"`
Name string `json:"name"`
// Reasons are Fides' own phrasings, e.g. "missing sbom".
Reasons []string `json:"reasons,omitempty"`
// WaivedReasons is what the waiver excused, present only on Waived.
WaivedReasons []string `json:"waived_reasons,omitempty"`
Reason string `json:"reason,omitempty"`
ApprovedBy string `json:"approved_by,omitempty"`
ExpiresAt string `json:"expires_at,omitempty"`
}
Control is one control the change gate judged.
Sent as an object rather than a bare key because the reasons are the useful part: "CC7.2" is a code to look up, "CC7.2 Vulnerability scanning: failed vuln-scan" is a thing to go and fix.
type Error ¶
Error is a refusal from the server, with the status kept so a caller can tell a rejected token from a trail that is not there.
type PolicyResult ¶
type PolicyResult struct {
Policy string `json:"policy"`
// Applies is false when the policy is conditional on a flow tag the trail
// does not carry. A policy that did not apply is not a policy that passed.
Applies bool `json:"applies"`
// Missing lists the attestation types the policy required and the trail
// does not have a compliant one of.
Missing []string `json:"missing,omitempty"`
}
PolicyResult is one environment policy's verdict on the trail.
type PolicyVerdict ¶
type PolicyVerdict struct {
Compliant bool `json:"compliant"`
TrailID string `json:"trail_id,omitempty"`
Results []PolicyResult `json:"results,omitempty"`
}
PolicyVerdict is the answer to "does this trail satisfy the environment's policy?"
Field names mirror Fides' own response. `compliant` rather than `passed`, and a per-policy breakdown rather than a flat list of violations, because a Gate that refuses a crossing has to be able to say which policy refused it.
func (PolicyVerdict) Unmet ¶
func (v PolicyVerdict) Unmet() []string
Unmet lists the policies that refused, so a failure can name them.
type SegregationOfDuties ¶
type SegregationOfDuties struct {
Committer string `json:"committer,omitempty"`
Approvers []string `json:"approvers,omitempty"`
Deployers []string `json:"deployers,omitempty"`
Compliant bool `json:"compliant"`
Violations []string `json:"violations,omitempty"`
}
SegregationOfDuties is Fides' four-eyes finding for a trail.
type UncountedError ¶ added in v0.4.0
type UncountedError struct {
// Kind is what Fides stored, e.g. "service". Anything but "session" is
// uncounted.
Kind string
// By is the identity the approval was meant to be attributed to.
By string
// Role is the role that was recorded.
Role string
}
UncountedError is an approval Fides stored but will never count.
The change gate approves on `no failing controls, no missing evidence, and at least one *human* approver`, and it counts an approver as human only when the stored `approver_kind` is `session`. A bearer token authenticates as a service, so Hecate's approvals only count when Fides honours the `on_behalf_of` delegation — which is default-deny, needs the server run with FIDES_DELEGATED_APPROVAL_ENABLED=true, and needs this token to hold Admin.
When it is not honoured the request still returns 201. The approval is real, it is simply invisible to the gate, and the promotion waits for a signature that has already been given. Worse, a service principal's stored identity is the literal string "service-account" against a UNIQUE(trail_id, approved_by) constraint, so an approver and a deployer recorded this way collapse into one row and four-eyes can never be satisfied.
None of that is visible from the status code, which is why this exists.
func (*UncountedError) Error ¶ added in v0.4.0
func (e *UncountedError) Error() string