masking

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Jun 17, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrPolicyNotFound = errors.New("masking policy not found for tenant")

ErrPolicyNotFound is returned by PolicyStore.Get when no policy is set for the requested tenant. Callers MAY treat this as "apply no masking" — the API GET handler distinguishes the case with a 404 so operators see when a policy is genuinely absent vs empty.

Functions

func IsSensitiveField

func IsSensitiveField(fieldName string) bool

IsSensitiveField checks if a field name indicates sensitive data

func SanitizeForLogging

func SanitizeForLogging(value any) any

SanitizeForLogging sanitizes a value for safe logging

Types

type FieldType

type FieldType string

FieldType represents the type of sensitive data

const (
	FieldTypeEmail      FieldType = "email"
	FieldTypePhone      FieldType = "phone"
	FieldTypeSSN        FieldType = "ssn"
	FieldTypeCreditCard FieldType = "credit_card"
	FieldTypePassword   FieldType = "password"
	FieldTypeAPIKey     FieldType = "api_key"
	FieldTypeIPAddress  FieldType = "ip_address"
	FieldTypeName       FieldType = "name"
	FieldTypeAddress    FieldType = "address"
	FieldTypeGeneric    FieldType = "generic"
)

type Masker

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

Masker handles data masking operations

func NewMasker

func NewMasker(config *MaskingConfig) *Masker

NewMasker creates a new data masker

func (*Masker) AddCustomRule

func (m *Masker) AddCustomRule(name string, pattern *regexp.Regexp, strategy MaskingStrategy)

AddCustomRule adds a custom masking rule

func (*Masker) ApplyCustomRules

func (m *Masker) ApplyCustomRules(text string) string

ApplyCustomRules applies custom masking rules to text

func (*Masker) ApplyStrategy

func (m *Masker) ApplyStrategy(value string, strategy MaskingStrategy, fieldType FieldType) string

ApplyStrategy masks value using strategy directly, bypassing the FieldType → strategy lookup. Used by the F3 per-tenant Policy machinery, which carries explicit (property-name → strategy) rules rather than the per-FieldType config Masker was designed around. fieldType is still passed because the StrategyTokenize path uses it to namespace the token cache; for non-tokenize strategies it's effectively a label.

func (*Masker) AutoMaskString

func (m *Masker) AutoMaskString(text string) string

AutoMaskString automatically detects and masks sensitive data in a string

func (*Masker) MaskAPIKey

func (m *Masker) MaskAPIKey(key string) string

MaskAPIKey masks an API key

func (*Masker) MaskCreditCard

func (m *Masker) MaskCreditCard(cc string) string

MaskCreditCard masks a credit card number

func (*Masker) MaskEmail

func (m *Masker) MaskEmail(email string) string

MaskEmail masks an email address

func (*Masker) MaskIPAddress

func (m *Masker) MaskIPAddress(ip string) string

MaskIPAddress masks an IP address

func (*Masker) MaskMap

func (m *Masker) MaskMap(data map[string]any) map[string]any

MaskMap masks sensitive fields in a map

func (*Masker) MaskPassword

func (m *Masker) MaskPassword(password string) string

MaskPassword always returns a fixed string for passwords

func (*Masker) MaskPhone

func (m *Masker) MaskPhone(phone string) string

MaskPhone masks a phone number

func (*Masker) MaskSSN

func (m *Masker) MaskSSN(ssn string) string

MaskSSN masks a social security number

func (*Masker) MaskString

func (m *Masker) MaskString(value string, fieldType FieldType) string

MaskString masks a string value based on field type

type MaskingConfig

type MaskingConfig struct {
	DefaultStrategy  MaskingStrategy
	FieldStrategies  map[FieldType]MaskingStrategy
	CustomPatterns   map[string]*regexp.Regexp
	ShowFirstChars   int  // For partial masking
	ShowLastChars    int  // For partial masking
	MaskChar         rune // Character to use for masking
	EnableAutoDetect bool // Auto-detect sensitive fields
}

MaskingConfig holds configuration for data masking

func DefaultMaskingConfig

func DefaultMaskingConfig() *MaskingConfig

DefaultMaskingConfig returns a secure default configuration

type MaskingRule

type MaskingRule struct {
	Pattern     *regexp.Regexp
	Strategy    MaskingStrategy
	ReplaceFunc func(string) string // Custom replacement function
}

MaskingRule defines a custom masking rule

type MaskingStrategy

type MaskingStrategy string

MaskingStrategy defines how data should be masked

const (
	StrategyFull     MaskingStrategy = "full"     // Replace entire value with mask
	StrategyPartial  MaskingStrategy = "partial"  // Show first/last N chars, mask middle
	StrategyHash     MaskingStrategy = "hash"     // Replace with SHA-256 hash
	StrategyRedact   MaskingStrategy = "redact"   // Replace with [REDACTED]
	StrategyTokenize MaskingStrategy = "tokenize" // Replace with consistent token
	StrategyNone     MaskingStrategy = "none"     // No masking
)

type Policy

type Policy struct {
	// TenantID is the tenant this policy applies to. PolicyStore keys
	// by tenant; this is the storage key duplicated in the value for
	// API-response convenience.
	TenantID string `json:"tenant_id"`

	// Properties is an explicit allow-list of property names to mask,
	// each with its chosen strategy. Highest priority — wins over
	// AutoDetect.
	Properties map[string]MaskingStrategy `json:"properties,omitempty"`

	// AutoDetect, when true, runs Masker's auto-detect heuristics on
	// any property NOT named in Properties. Lower priority than the
	// explicit list. Off by default — opt-in for tenants that want
	// pattern-based masking without enumerating every property.
	AutoDetect bool `json:"auto_detect"`

	// UpdatedAt is the wall-clock time the policy was last written.
	// Surfaced in GET responses so operators can correlate with audit
	// events.
	UpdatedAt time.Time `json:"updated_at"`
}

Policy is a per-tenant masking specification. Operators set a Policy per tenant via the F3 compliance API; the Server applies it to every outgoing node/edge response on the REST read path.

Schema is property-name-keyed: an operator names specific properties (e.g. "email", "ssn") and chooses a MaskingStrategy for each. If AutoDetect is true and a property is NOT named in Properties, the Masker's heuristics (regex-based: email/phone/cc/ssn/apikey/ip) get a second-pass shot at it.

A nil or empty Policy is equivalent to "no masking" — the property flows through verbatim. This is deliberate: the default for a tenant with no policy set is the pre-F3 behaviour (everything visible), matching the design doc §3 Decision 4's "policies are lost on restart" caveat (in-memory PolicyStore — a restart resets every tenant to "no policy" until they re-POST).

func (*Policy) Apply

func (p *Policy) Apply(props map[string]any, masker *Masker) map[string]any

Apply returns a deep copy of props with this Policy's masking rules applied. Resolution order per property name:

  1. Policy.Properties[name] — explicit operator-set strategy.
  2. If AutoDetect is true, Masker.detectFieldType identifies the name's likely sensitivity (email/phone/ssn/...) and applies that FieldType's configured strategy.
  3. Otherwise the value flows through unmasked.

Non-string property values whose name appears in Policy.Properties are coerced to string for masking — operator named the property, they expect it masked. Numbers/bools/maps not named in Properties flow through unchanged (auto-detect is regex-based and only fires on strings).

Apply does NOT mutate the input map. Returns a new map with the same shape; callers can freely store/serialize the result.

masker is the application's shared Masker instance. Passed in rather than constructed because Masker holds a token cache for StrategyTokenize — re-instantiating per call would lose token consistency across requests for the same value.

Nil receiver (no policy for this tenant): returns props unchanged — the caller can save the copy if they want, but Apply skips the work.

func (*Policy) ApplyToStorageValues

func (p *Policy) ApplyToStorageValues(
	props map[string]storage.Value, masker *Masker,
) map[string]storage.Value

ApplyToStorageValues is the storage.Value-typed twin of Apply, used by GraphQL response paths that iterate node.Properties / edge.Properties directly (i.e., not via the REST nodeToResponse helper, which works on map[string]any after the storage layer's value-decoding step).

Behavioural contract matches Apply: nil receiver, empty input, or nil masker each return props unchanged. The masker-nil case is a defensive audit-observable gap rather than a fail-closed crash; F3 design doc §6 requires read paths never break on masking misconfiguration — better to surface an unmasked response and log the gap.

Masked output is always TypeString because the masking strategies (StrategyPartial / StrategyTokenize / StrategyHash / StrategyFull / StrategyRandom) emit strings. A TypeInt value masked under StrategyFull becomes a TypeString containing "***" (or the configured replacement). This mirrors Apply's behaviour (its any-typed return is also a string post-masking) and keeps the policy semantics type-independent: "operator named this property → it's masked, however it was typed."

The returned map is freshly allocated; the input map is not mutated. Unmasked entries share their storage.Value (which is a value type holding a []byte slice header, not a deep copy of the bytes).

func (*Policy) Clone

func (p *Policy) Clone() *Policy

Clone returns a deep copy of the Policy. PolicyStore uses this to hand out snapshots that callers can mutate without affecting the store's authoritative copy.

type PolicyStore

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

PolicyStore is the in-memory tenant → Policy registry. Per design doc §3 Decision 4a, this is in-memory only: policies are lost on restart until the operator re-POSTs them. A snapshot-persisted upgrade (Decision 4b) is the next-PR option if/when commercial direction lands as "hosted."

Concurrency: a single sync.RWMutex protects the map. The expected access pattern is heavily-read (every read-path response checks the policy) and rarely-written (operators set policies on tenant provisioning, change rarely). The shard-per-tenant idiom used for nodes/edges in pkg/storage (gs.nodeShards [256]) is overkill here because the tenant count is small and writes are rare.

func NewPolicyStore

func NewPolicyStore() *PolicyStore

NewPolicyStore constructs an empty PolicyStore.

func (*PolicyStore) Delete

func (s *PolicyStore) Delete(tenantID string) bool

Delete removes the policy for tenantID. Returns true if a policy was present, false if no-op (already absent).

func (*PolicyStore) Get

func (s *PolicyStore) Get(tenantID string) (*Policy, error)

Get returns the policy for tenantID, or (nil, ErrPolicyNotFound) if none is set. The returned *Policy is a deep clone — callers can mutate it freely without affecting the store.

func (*PolicyStore) Set

func (s *PolicyStore) Set(tenantID string, p *Policy)

Set installs or replaces the policy for tenantID. The TenantID field on the policy is set from the argument (so callers can't accidentally store policy-for-A under key-B). UpdatedAt is stamped to now.

The store retains a deep clone — the caller's *Policy is theirs to mutate after the call returns.

func (*PolicyStore) Tenants

func (s *PolicyStore) Tenants() []string

Tenants returns the sorted set of tenant IDs that have a policy set. Used by admin diagnostics; not exposed via the F3 endpoints in PR-3a.

Jump to

Keyboard shortcuts

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