The tickets array was never broken. Every ticket in the reported batch targeted a client repo, and the I1 gate refuses a confidential capture through a us-nexus origin at any length — reproduced live: length 1 and 20 both fine for an internal repo, length 1 refused for a client one. What made it look like an array bug is that the refusal named neither the offending target nor that a policy had said no, so with 20 tickets in flight "the array is broken" was a reasonable conclusion. The refusal now names the targets carrying the effective classification, and tells a caller who declared confidential themselves exactly that. Regression tests at length 1 and 20 pin the path that was already correct; a third pins per-item partial success. Closes #91 part 1. Co-authored-by: Mathias <mthbqv@gmail.com>
This commit was merged in pull request #95.
This commit is contained in:
@@ -83,7 +83,7 @@ func (s *Service) Capture(ctx context.Context, in CaptureInput) (CaptureReceipt,
|
||||
declared, _ = classification.ParseLevel(in.Context.Classification)
|
||||
}
|
||||
|
||||
effective, securityEvents := s.resolveClassification(declared, in)
|
||||
effective, securityEvents, escalators := s.resolveClassification(declared, in)
|
||||
|
||||
// Server-derived origin governs the I1 gate; a caller-asserted harness
|
||||
// label that names a different zone is descriptive-only and logged as a
|
||||
@@ -107,8 +107,8 @@ func (s *Service) Capture(ctx context.Context, in CaptureInput) (CaptureReceipt,
|
||||
Items: nil, // refused before any write
|
||||
SecurityEvents: append(securityEvents, "I1 refusal: confidential capture via us-nexus origin"),
|
||||
}, AuditCentral)
|
||||
return CaptureReceipt{}, fmt.Errorf("%w: effective classification confidential through %s origin",
|
||||
ErrSovereigntyRefused, in.Context.Origin)
|
||||
return CaptureReceipt{}, fmt.Errorf("%w: effective classification confidential through %s origin (%s)",
|
||||
ErrSovereigntyRefused, in.Context.Origin, escalationReason(escalators))
|
||||
}
|
||||
|
||||
receipt := CaptureReceipt{
|
||||
@@ -221,15 +221,28 @@ func (s *Service) validate(in CaptureInput) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveClassification computes the effective level (stricter of
|
||||
// declared and every target's derived level) and collects a security
|
||||
// event whenever the caller under-declared relative to a target floor.
|
||||
func (s *Service) resolveClassification(declared classification.Level, in CaptureInput) (classification.Level, []string) {
|
||||
// resolveClassification computes the effective level (stricter of declared
|
||||
// and every target's derived level), collects a security event whenever the
|
||||
// caller under-declared relative to a target floor, and names the targets
|
||||
// that carry the strictest level.
|
||||
//
|
||||
// That third return is what makes an I1 refusal actionable. A caller sending
|
||||
// twenty tickets gets one refusal; without the offending target it cannot
|
||||
// tell which repo tripped the gate, and infra#265 shows what happens next —
|
||||
// the caller concluded the tickets ARRAY was broken, built a fallback of
|
||||
// twenty individual calls, and filed a bug against the wrong thing.
|
||||
func (s *Service) resolveClassification(declared classification.Level, in CaptureInput) (classification.Level, []string, []string) {
|
||||
effective := declared
|
||||
var events []string
|
||||
derivedFor := map[string]classification.Level{}
|
||||
var order []string
|
||||
consider := func(kind classification.TargetKind, name string) {
|
||||
derived := s.policy.Derive(classification.Target{Kind: kind, Name: name})
|
||||
effective = classification.Stricter(effective, derived)
|
||||
if _, seen := derivedFor[name]; !seen {
|
||||
derivedFor[name] = derived
|
||||
order = append(order, name)
|
||||
}
|
||||
if declared < derived {
|
||||
events = append(events, fmt.Sprintf("classification under-declared: declared=%s target=%s(%s) derived=%s",
|
||||
declared, name, kindString(kind), derived))
|
||||
@@ -241,7 +254,28 @@ func (s *Service) resolveClassification(declared classification.Level, in Captur
|
||||
for _, tk := range in.Tickets {
|
||||
consider(classification.RepoTarget, tk.Repo)
|
||||
}
|
||||
return effective, events
|
||||
|
||||
// Only targets that actually reach the effective level are named: at
|
||||
// twenty tickets, listing the innocent ones is noise that hides the one
|
||||
// that matters.
|
||||
var escalators []string
|
||||
for _, name := range order {
|
||||
if derivedFor[name] == effective && effective > declared {
|
||||
escalators = append(escalators, name)
|
||||
}
|
||||
}
|
||||
return effective, events, escalators
|
||||
}
|
||||
|
||||
// escalationReason renders why the effective classification landed where it
|
||||
// did, for an operator reading a refusal. Naming the caller's own declaration
|
||||
// matters as much as naming a target: "you asked for this" and "a repo you
|
||||
// touched required it" call for different fixes.
|
||||
func escalationReason(escalators []string) string {
|
||||
if len(escalators) == 0 {
|
||||
return "declared by the caller; no target required it"
|
||||
}
|
||||
return "required by: " + strings.Join(escalators, ", ")
|
||||
}
|
||||
|
||||
func (s *Service) persistInsight(ctx context.Context, ins Insight) (InsightResult, string, error) {
|
||||
|
||||
Reference in New Issue
Block a user