Documentation
¶
Index ¶
- Variables
- func IsSensitiveField(fieldName string) bool
- func SanitizeForLogging(value any) any
- type FieldType
- type Masker
- func (m *Masker) AddCustomRule(name string, pattern *regexp.Regexp, strategy MaskingStrategy)
- func (m *Masker) ApplyCustomRules(text string) string
- func (m *Masker) ApplyStrategy(value string, strategy MaskingStrategy, fieldType FieldType) string
- func (m *Masker) AutoMaskString(text string) string
- func (m *Masker) MaskAPIKey(key string) string
- func (m *Masker) MaskCreditCard(cc string) string
- func (m *Masker) MaskEmail(email string) string
- func (m *Masker) MaskIPAddress(ip string) string
- func (m *Masker) MaskMap(data map[string]any) map[string]any
- func (m *Masker) MaskPassword(password string) string
- func (m *Masker) MaskPhone(phone string) string
- func (m *Masker) MaskSSN(ssn string) string
- func (m *Masker) MaskString(value string, fieldType FieldType) string
- type MaskingConfig
- type MaskingRule
- type MaskingStrategy
- type Policy
- type PolicyStore
Constants ¶
This section is empty.
Variables ¶
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 ¶
IsSensitiveField checks if a field name indicates sensitive data
func SanitizeForLogging ¶
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 (*Masker) AddCustomRule ¶
func (m *Masker) AddCustomRule(name string, pattern *regexp.Regexp, strategy MaskingStrategy)
AddCustomRule adds a custom masking rule
func (*Masker) ApplyCustomRules ¶
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 ¶
AutoMaskString automatically detects and masks sensitive data in a string
func (*Masker) MaskAPIKey ¶
MaskAPIKey masks an API key
func (*Masker) MaskCreditCard ¶
MaskCreditCard masks a credit card number
func (*Masker) MaskIPAddress ¶
MaskIPAddress masks an IP address
func (*Masker) MaskPassword ¶
MaskPassword always returns a fixed string for passwords
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 ¶
Apply returns a deep copy of props with this Policy's masking rules applied. Resolution order per property name:
- Policy.Properties[name] — explicit operator-set strategy.
- If AutoDetect is true, Masker.detectFieldType identifies the name's likely sensitivity (email/phone/ssn/...) and applies that FieldType's configured strategy.
- 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).
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.