policy

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package policy bounds what chrome-cdp may drive: which origins it will act on, which verbs are permitted there, and which local paths may be uploaded.

The package is deliberately pure — no I/O, no context, no browser — so the whole decision table can be exercised exhaustively in tests. Enforcement lives at the command boundary in internal/cli, which consults a Checker before calling into chrome.Browser; putting it in the driver would mean every future Browser method had to remember to check.

Two properties are load-bearing, and any change must preserve both:

  • Matching is on the PARSED URL's host, never on a substring of the raw URL. "*.example.com" matches a.example.com and a.b.example.com but NOT example.com (which needs its own entry), notexample.com, or example.com.evil.io. Those suffix-confusion cases are exactly where a naive implementation is wrong, and they are the first tests in the suite.

  • Every ambiguity fails CLOSED. deny beats allow; an unclassified verb is mutating; an origin that cannot be parsed is refused whenever the checker is active AT ALL — a deny-list can no more prove a URL is not the denied origin than an allow-list can prove it is — and a pattern that does not parse is a fatal config error rather than a rule that silently matches nothing. A policy that fails open is worse than no policy.

Index

Constants

View Source
const (
	OnError  = "error"
	OnPrompt = "prompt"
)

Violation modes.

Variables

This section is empty.

Functions

func KnownVerbs

func KnownVerbs() []string

KnownVerbs returns every classified verb, sorted — used in error messages.

func Label

func Label(rawURL string) string

Label renders a URL the way a log record, a refusal message, and a redacted tab row all need it: its origin when it has one, and otherwise the scheme plus a placeholder.

It never returns the raw URL. That is the point of it: a URL's query string carries values — session tokens, ids, search terms — and RFC-0012 forbids the audit log from recording values at all. A fallback that returned the string unchanged would put exactly the URLs a policy refused, in full, into the append-only file the feature exists to make safe.

func Verbs

func Verbs() map[string]Class

Verbs returns a copy of the classification table, for the coverage guard in internal/cli.

Types

type Checker

type Checker struct {
	// contains filtered or unexported fields
}

Checker answers policy questions about a parsed configuration. The zero value is an inert checker that permits everything; use New to build a real one.

func New

func New(cfg Config) (*Checker, error)

New parses cfg into a Checker.

A parse failure is FATAL and the caller must refuse to run — unlike the rest of this repo's config handling, which warns and continues. A malformed pattern that was skipped with a warning would be a rule the user believes is in force and is not, which is the failure mode this whole feature exists to prevent (RFC-0012 VS-15).

func (*Checker) Active

func (c *Checker) Active() bool

Active reports whether the checker will refuse anything at all. A caller uses it to keep behaviour byte-identical when no policy is configured (US-5), and to decide whether `list` should redact non-allowed tabs (VS-17).

func (*Checker) AuditAll

func (c *Checker) AuditAll() bool

AuditAll reports whether permitted actions are logged too, not just refusals.

func (*Checker) AuditLog

func (c *Checker) AuditLog() string

AuditLog returns the configured audit-log path ("" for none).

func (*Checker) Check

func (c *Checker) Check(rawURL, verb string) Decision

Check decides whether verb may run against the tab (or destination) at rawURL.

The order is the fail-closed order and must not be rearranged: verbs_denied, then deny, then allow, then read_only.

func (*Checker) OnViolation

func (c *Checker) OnViolation() string

OnViolation reports the configured violation mode ("error" or "prompt").

func (*Checker) OriginAllowed

func (c *Checker) OriginAllowed(rawURL string) bool

OriginAllowed reports whether an origin passes the allow/deny rules alone, ignoring the verb. `list` uses it to redact tabs the policy does not cover.

It agrees with Check on the fail-closed reading: a URL with no identifiable origin is not allowed under an active policy, whatever the shape of the rules. A file:// or data: tab is exactly the tab the policy covers least, so it is the last one whose full URL and title should be handed back.

func (*Checker) Source

func (c *Checker) Source() string

Source returns the config file the policy came from ("" when unknown).

func (*Checker) UploadRoots

func (c *Checker) UploadRoots() []string

UploadRoots returns the configured upload roots, as written (RFC-0006).

This package deliberately stops here and does NOT decide whether a path is under a root. The only honest root comparison resolves symlinks on both sides — on macOS /tmp and /var are themselves symlinks, so a configured "/tmp/receipts" and the real path of a file inside it are different strings — and resolving symlinks is I/O, which does not belong in a pure decision package. `upload` does that comparison where the I/O already is. Two implementations of one security rule would be worse than none.

type Class

type Class int

Class is what a command does to the page, which decides how it is checked.

Mutating is the zero value on purpose: a verb missing from the table is treated as mutating, so forgetting to classify a new verb over-restricts rather than silently opening a hole (RFC-0012 VS-6).

const (
	// Mutating verbs act on the page: checked against allow/deny, read_only,
	// and verbs_denied.
	Mutating Class = iota
	// Reading verbs only observe the page: checked against allow/deny and
	// verbs_denied, but permitted on a read_only origin.
	Reading
	// Exempt commands never touch page content — tab lifecycle, batch, and
	// meta verbs. They are not origin-checked at all.
	Exempt
)

func Classify

func Classify(verb string) (Class, bool)

Classify returns a verb's class and whether it was declared in the table. An undeclared verb is reported as Mutating — the fail-closed default.

func (Class) String

func (c Class) String() string

type Config

type Config struct {
	// Present reports that a [policy] table existed at all. With no table the
	// whole layer is inert and nothing about the CLI changes (US-5).
	Present bool
	// Enabled is the master switch. A present table defaults to enabled; set
	// enabled = false to keep a policy on file without applying it.
	Enabled bool

	Allow       []string
	Deny        []string
	ReadOnly    []string
	VerbsDenied []string
	UploadRoots []string

	AuditLog string
	AuditAll bool
	// OnViolation is "error" (default) or "prompt".
	OnViolation string

	// RequireAllow refuses everything when Allow is empty, instead of treating
	// an empty allow-list as "all except deny". The CLI leaves this false —
	// adding a feature must not change existing behaviour — and MCP mode
	// (RFC-0004) will set it, which is the whole point of expressing it here.
	RequireAllow bool

	// Source is the config file the table came from, echoed in refusals so a
	// user knows which file to edit.
	Source string
}

Config is the parsed policy table. It is plain data so internal/config can fill it from TOML and the CLI can overlay --allow / --policy-off before handing it to New.

type Decision

type Decision struct {
	Allowed bool
	Rule    string
	Reason  string
}

Decision is the outcome of a check. Rule names the entry that decided it, in the form the user wrote it ("deny: admin.corp.local"), so a refusal points at the line to edit rather than at a guess.

type Origin

type Origin struct {
	Scheme string // lowercased; "" when the URL carried none
	Host   string // lowercased, with the FQDN root dot stripped
	Port   string // canonical digits ("0443" → "443"); "" when the URL omitted it
}

Origin is the scheme/host/port a decision is made about — the parsed form of a tab's URL. Nothing in this package ever looks at the raw URL string.

func ParseOrigin

func ParseOrigin(rawURL string) (Origin, error)

ParseOrigin extracts the origin from a URL, as CHROME would resolve it.

The "as Chrome would resolve it" is the whole job. A policy decides about the origin the browser will actually act on, so any disagreement between Chrome's parse and this one is a bypass, in whichever direction it happens to fall. Two disagreements are known and handled here, by canonicalize:

  • Nesting schemes. view-source:, blob:, and filesystem: carry a whole URL in their opaque part, so net/url reports no host for them while Chrome serves the inner origin's authenticated content. They are unwrapped, so the INNER origin is what gets decided about — which cuts both ways: view-source: of an allowed origin stays allowed.

  • Backslashes in the authority. Chrome normalises `\` to `/` there, so https://bank.example\@evil.io/ is bank.example with the path /@evil.io/, while net/url rejects it outright as invalid userinfo.

It still fails for anything with no host at all — about:blank, data:, file://, javascript:, chrome:// — and the caller treats that failure as a refusal whenever a policy is active, because a rule cannot decide about an origin it cannot identify.

func (Origin) String

func (o Origin) String() string

String renders the origin the way a refusal message names it: the host, plus the port when the URL carried one explicitly.

type Pattern

type Pattern struct {
	Raw      string // as written, for the rule string in a refusal
	Scheme   string // "" matches any scheme
	Host     string // for a wildcard, the suffix WITHOUT the leading "*."
	Port     string // "" or "*" matches any port; otherwise canonical digits
	Wildcard bool

	// MatchApex makes a "*.host" wildcard cover the apex `host` as well.
	//
	// It is set for `deny` entries and only for them, because the same exclusion
	// is right in one list and a trap in the other. In `allow`, excluding the
	// apex is the strict reading a boundary needs: `*.example.com` must not
	// quietly widen to example.com itself. In `deny` it is a hole — a user who
	// writes deny = ["*.bank.example"] means "not my bank", and reading that as
	// "every subdomain of my bank, but the bank itself is fine" protects them
	// everywhere except the one host they were thinking of. Over-blocking is the
	// safe direction in a deny-list, so the wildcard is apex-inclusive there.
	MatchApex bool
}

Pattern is one parsed origin pattern: an exact host, or a "*." subdomain wildcard, each with an optional scheme and an optional port.

Deliberately not a regex. A policy language that is hard to read is a policy that is wrong without anyone noticing.

func ParsePattern

func ParsePattern(s string) (Pattern, error)

ParsePattern parses one pattern of the form [scheme://]host[:port], where host is either an exact host or "*." followed by a suffix.

It is strict on purpose: anything it cannot represent exactly is an error, so a user never ends up with a rule that matches something other than what they wrote. In particular a bare "*", an interior "*", a path, and a userinfo or query fragment are all rejected rather than being interpreted generously.

func (Pattern) Match

func (p Pattern) Match(o Origin) bool

Match reports whether o satisfies the pattern.

The wildcard case is the one that matters: HasSuffix alone would let "notexample.com" match "*.example.com", so the dot is part of the compared suffix and the matched host must be strictly longer than "." + suffix, which is what excludes the bare "example.com" and a hostile ".example.com".

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL