sensitive

package module
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Feb 18, 2025 License: MIT Imports: 10 Imported by: 3

README

struct-sensitive

Coverage Status GoDoc

This Go library leverages struct tags to identify and manage sensitive fields in structs, ensuring data protection and compliance with privacy standards.

In simple terms, it provides Redact and Mask utilities for handling fields that contain sensitive data such as PII, PHI, PCI, SSN.

Installation

go get github.com/ln80/struct-sensitive
import (
    sensitive "github.com/ln80/struct-sensitive"
)

Basic usage

Here's a basic example of how to use the library:

type Device struct {
    IP string `pii:"data,kind=ipv4_addr"`
}

type Profile struct {
    Email    string `sensitive:"data,kind=email"`
    Fullname string `sensitive:"data"`
    Device   Device `sensitive:"dive"`
}

var profile = Profile{
    Email:    "eric.prosacco@example.com",
    Fullname: "Eric Prosacco",
    Device: Device{
        IP: "28.175.98.7",
    },
}

_ = sensitive.Mask(&profile)

// Output:
// Profile{
//   Email: "*************@example.com",
//   Fullname: "*************",
//   Device: Device{
//    IP: "28.175.98.***",
//   },
// }
Tags basic usage:
  • sensitive:data indicates that the field contains sensitive data and may also specify its kind (optional).

  • sensitive:dive specifies that the nested struct or the collection of structs contains sensitive fields.

  • sensitive:subjectID marks the field value as the subject identifier to whom the sensitive data belongs. Only one subject ID value is authorized at the struct level when required.

Example of registering a default mask for a particular sensitive data kind (e.g., 'be_nrn'):

import (
    "github.com/ln80/struct-sensitive/mask"
)

...

var defaultMask := func(val string) (masked string, err error) {
    // TODO implement 'be_nrn' mask behavior here
    masked = "**.**.**-***-**"
    return
}

mask.Register("be_nrn", defaultMask)

For more usage and examples see the Godoc.

Features

  • Provides functions for masking, redacting, and scanning sensitive data
  • Includes a set of predefined masks
  • Customizable behaviors through options and callbacks
  • Supports multiple tag IDs: sensitive, pii, sens that can be used interchangeably.
Predefined masks:
  • email
  • ipv4_addr
  • credit_card
  • fullname

Limitations

  1. Only fields of types convertible to string or *string are supported, although nesting structs directly or through collections (slices and maps) is also supported.

  2. Self-Referencing Types are supported, allowing types to include fields of the same type. However, Self-Referencing Values (instances that create a reference loop) are not supported.

  3. At the moment, collections of types convertible to string or *string are not supported.

Documentation

Overview

Package sensitive provides a set functions to handle sensitive fields in structs, including:

  • Mask partially redacts sensitive data while preserving the format of the data type (aka kind). It uses a set of predefined masks (e.g 'email' 'ipv4_addr') and allows to register additional masks.

  • Redact replaces sensitive field values with a redaction symbol ('*') by default. The behavior can be customized through optional parameters.

  • Scan is a lower-level function that gives access to sensitive struct metadata and a fields replacer. This can be used to implement more advanced features such as client-side encryption.

  • Check determines whether a struct contains any sensitive data fields.

Package sensitive leverages Go struct tags to identify and categorize struct sensitive field. It supports the following tag IDs `sensitive`, `pii`, `sens`. Here's an example:

type Profile struct {
	Email    string `sensitive:"data,kind=email"`
	Fullname string `sensitive:"data,kind=name"`
	Role     string
}

Applying the default masking logic:

var profile := Profile{
	Email:    "eric.prosacco@example.com",
	Fullname: "Eric Prosacco",
	Role:     "Teacher",
}

_ = sensitive.Mask(&profile)

// After masking:
//
// Profile{
//   Email: "****.********@example.com",
//   Fullname: "*************",
//   Role: "Teacher",
// }

// Notes:
// - The default behavior of the `email` mask is to hide the local part while revealing the domain part.
// - The library does not provide a default mask for `name`; therefore, the default redaction behavior is applied.
// - You may consider defining a specific `name` mask and registering it using [mask.Register].
// - The Role field is not tagged as sensitive, so it remains unchanged.

Applying a custom redact logic:

type Profile struct {
	Email    string `sensitive:"data,kind=email"`
	Fullname string `sensitive:"data,kind=name"`
	Role     string
}

var profile := Profile{
	Email:    "eric.prosacco@example.com",
	Fullname: "Eric Prosacco",
	Role:     "Teacher",
}

option := func(rc *sensitive.RedactConfig) {
	rc.RedactFunc = func(fr sensitive.FieldReplace, val string) (string, error) {
		switch fr.Kind {
		case "email":
			return "ghost@unknown.net", nil
		case "name":
			return "Ghost", nil
		}
		return strings.Repeat("*", len(val)), nil
	}
}

_ = sensitive.Redact(&profile, option)

// After redacting:
//
// Profile{
//   Email: "ghost@unknown.net",
//   Fullname: "Ghost",
//   Role: "Teacher",
// }

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidTagConfiguration = errors.New("invalid 'sensitive' tag configuration")
	ErrUnsupportedType         = errors.New("unsupported 'sensitive' type")
	ErrUnsupportedFieldType    = errors.New("'sensitive' field type must be convertible to string")
	ErrMultipleNestedSubjectID = errors.New("potential multiple nested subject IDs")
	ErrSubjectIDNotFound       = errors.New("subject ID is not found")
)
View Source
var (
	ErrFailedToMaskCopy = errors.New("failed to mask copy")
)
View Source
var (
	ErrRedactFuncNotFound = errors.New("redact function not found")
)

Functions

func Check

func Check(v any) (found bool, err error)

Check verifies whether the provided struct contains sensitive data fields. It returns an error if the 'sensitive' tag is misconfigured or if the value parameter is not a struct or a pointer to a struct.

func Mask

func Mask(structPtr any, opts ...func(*RedactConfig)) error

Mask partially redacts sensitive data based on their type (aka kind).

It is simply a facade function that calls Redact with WithRegisteredMasks option.

Use mask.Register to register additional masks.

Example

Example of masking with a custom registered mask (ex: Belgian National Register Number)

package main

import (
	"errors"
	"log"
	"regexp"

	sensitive "github.com/ln80/struct-sensitive"
	"github.com/ln80/struct-sensitive/internal/option"
	"github.com/ln80/struct-sensitive/mask"
)

func main() {

	type Profile struct {
		Email    string `sensitive:"data,kind=email"`
		NRN      string `sensitive:"data,kind=be_nrn"`
		Fullname string `sensitive:"data"`
	}

	// Define the Belgian National Register Number (be_nrn) mask behavior.
	//
	// Assuming, based on business requirements, revealing the birth date is acceptable by default.
	//
	// You can evolve the mask behavior by adding options to the struct.
	// However, note that only the default options are used by the [sensitive.Mask] function.
	// Alternatively, you can define and apply the mask directly in your code if you need custom behavior for specific cases.
	type BeNRNConfig struct {
		RevealBirthDate bool
	}

	BeNRNRegex := regexp.MustCompile(`^(\d{2})\.(\d{2})\.(\d{2})-(\d{3})-(\d{2})$`)

	BeNRNMask := func(val string, opts ...func(*mask.Config[BeNRNConfig])) (masked string, err error) {
		cfg := mask.DefaultConfig(BeNRNConfig{
			RevealBirthDate: true,
		})
		option.Apply(&cfg, opts)

		matches := BeNRNRegex.FindStringSubmatch(val)
		if matches == nil {
			return "", errors.New("invalid BE_NRN format")
		}

		if !cfg.Kind.RevealBirthDate {
			masked = "**.**.**-***-**"
			return
		}
		masked = matches[1] + "." + matches[2] + "." + matches[3] + "-***-**"
		return
	}

	mask.Register("be_nrn", mask.DefaultMasker(BeNRNMask))

	p := Profile{
		Email:    "eric.prosacco@example.com",
		NRN:      "85.12.25-123-45",
		Fullname: "Eric Prosacco",
	}

	err := sensitive.Mask(&p)
	if err != nil {
		log.Fatal(err)
	}

	print(p)

}
Output:

Profile{
  Email: "*************@example.com",
  NRN: "85.12.25-***-**",
  Fullname: "*************",
}

func Redact

func Redact(structPtr any, opts ...func(*RedactConfig)) error

Redact redacts sensitive data from struct field values by replacing each character with '*'.

It returns an error if the value is not a struct pointer, the 'sensitive' tag is misconfigured, or if the redact function is nil.

Optionally, you can override the default redact function by passing a custom one.

Example

Example of a basic usage

package main

import (
	"log"

	sensitive "github.com/ln80/struct-sensitive"
)

func main() {
	type Profile struct {
		Email    string `sensitive:"data"`
		Fullname string `sensitive:"data"`
		Role     string
	}

	p := Profile{
		Email:    "eric.prosacco@example.com",
		Fullname: "Eric Prosacco",
		Role:     "Teacher",
	}

	err := sensitive.Redact(&p)
	if err != nil {
		log.Fatal(err)
	}

	print(p)

}
Output:

Profile{
  Email: "*************************",
  Fullname: "*************",
  Role: "Teacher",
}
Example (Second)

Example of a custom Redact function

package main

import (
	"log"
	"strings"

	sensitive "github.com/ln80/struct-sensitive"
)

func main() {
	type Profile struct {
		Email    string `sensitive:"data,kind=email"`
		Fullname string `sensitive:"data,kind=name"`
		Role     string
	}

	p := Profile{
		Email:    "eric.prosacco@example.com",
		Fullname: "Eric Prosacco",
		Role:     "Teacher",
	}

	err := sensitive.Redact(&p, func(rc *sensitive.RedactConfig) {
		rc.RedactFunc = func(fr sensitive.FieldReplace, val string) (string, error) {
			switch fr.Kind {
			case "email":
				return "ghost@unknown.net", nil
			case "name":
				return "Ghost", nil
			}
			return strings.Repeat("*", len(val)), nil
		}
	})
	if err != nil {
		log.Fatal(err)
	}

	print(p)

}
Output:

Profile{
  Email: "ghost@unknown.net",
  Fullname: "Ghost",
  Role: "Teacher",
}

func RedactDefaultFunc

func RedactDefaultFunc(_ FieldReplace, val string) (string, error)

func WithRegisteredMasks

func WithRegisteredMasks(rc *RedactConfig)

WithRegisteredMasks returns an option that force redaction using the registered masks, including the predefined one e.g. `email`, `ipv4_addr`, `credit_card`.

Use mask.Register to override or register new masks.

Types

type FieldReplace

type FieldReplace struct {
	// SubjectID is the identifier for the sensitive data subject, resolved at the struct level.
	// This field may be empty if it is not required by the calling function.
	SubjectID string

	// Name is the name of the sensitive field.
	Name string

	// RType is the original type of the sensitive field.
	// Note that this type must be convertible to a string.
	RType reflect.Type

	// Kind is the user-defined type of sensitive data, defined as an option in the 'sensitive' tag.
	Kind string

	// Options are the options specified in the 'sensitive' tag.
	Options TagOptions
}

FieldReplace contains metadata for sensitive fields.

type Masked added in v0.6.0

type Masked[T any] struct {
	// contains filtered or unexported fields
}

Masked is a wrapper that contains both the original value and masked copy

func MaskedCopy added in v0.6.0

func MaskedCopy[T any](v T, opts ...func(*MaskedCopyConfig)) *Masked[T]

MaskedCopy returns a masked copy of the given value. It panics in case of failure.

func NewMaskedCopy added in v0.6.0

func NewMaskedCopy[T any](v T, opts ...func(*MaskedCopyConfig)) (*Masked[T], error)

NewMaskedCopy returns a new masked copy of the given value. It fails if it can't copy the value or the mask config is invalid.

func (Masked[T]) LogValue added in v0.6.0

func (r Masked[T]) LogValue() slog.Value

LogValue implements slog.LogValuer

func (Masked[T]) MarshalJSON added in v0.6.0

func (r Masked[T]) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler

func (Masked[T]) Reveal added in v0.6.0

func (r Masked[T]) Reveal() T

Reveal reveals the original value without applying masks

func (Masked[T]) String added in v0.6.0

func (r Masked[T]) String() string

String implements fmt.Stringer

func (Masked[T]) Value added in v0.6.0

func (r Masked[T]) Value() T

Value returns the masked copy value

type MaskedCopyConfig added in v0.6.0

type MaskedCopyConfig struct {
	DeepCopy bool // default false
}

type RedactConfig

type RedactConfig struct {
	// RequireSubjectID force the subjectID resolution from the struct value.
	// This config is disabled by default.
	RequireSubjectID bool

	// RedactFunc overrides the default redaction function `RedactDefaultFunc`.
	RedactFunc ReplaceFunc
}

RedactConfig presents the configuration required by `sensitive.Redact`.

type ReplaceFunc

type ReplaceFunc func(fr FieldReplace, val string) (string, error)

ReplaceFunc is a callback function executed by the [Struct.Replace] method. It receives the original value of the sensitive field, converted to a string, and returns the new value as a string along with any error that may occur.

type Struct

type Struct interface {
	// Replace accepts a replacement function and applies it to each sensitive data field.
	Replace(fn ReplaceFunc) error

	// SubjectID returns the resolved SubjectID of the sensitive struct.
	// It panics if the SubjectID is not resolved.
	SubjectID() string

	// HasSensitive indicates whether the struct contains sensitive data fields.
	HasSensitive() bool
	// contains filtered or unexported methods
}

Struct provides an accessor for sensitive struct fields and subject identifiers.

func Scan

func Scan(v any, requireSubject bool) (accessor Struct, err error)

Scan inspects the given value and returns an accessor for the sensitive struct. It returns an error if the value is not a pointer to a struct or if the 'sensitive' tag is misconfigured.

The Struct accessor and the Scan function are low-level components. In most cases, you should consider using the Redact or Mask functions instead.

type TagOptions

type TagOptions map[string]string

TagOptions presents a map of options configured at the `sensitive` tag.

func (TagOptions) Get

func (m TagOptions) Get(name string) string

type TagPayload

type TagPayload struct {
	// ID is the identifier of the tag, e.g., `sensitive`, `pii`.
	ID string

	// Name is the name of the sensitive tag, e.g., `data`, `subjectID`.
	Name string

	// Options represents the options associated with the sensitive tag.
	Options TagOptions
}

TagPayload represents the metadata for a sensitive tag.

func FieldTag

func FieldTag(v any, field string) (*TagPayload, error)

FieldTag extracts and parses the `sensitive` tag of the specified field in the given struct.

It returns an error if the value is neither a struct nor a pointer to a struct, or if the field is not found. It returns an empty value if the `sensitive` tag is misconfigured.

func MustFieldTag

func MustFieldTag(v any, field string) TagPayload

MustFieldTag extracts and parses the `sensitive` tag of the specified field in the given struct.

It panics if the value is neither a struct nor a pointer to a struct, if the field is not found, or if the `sensitive` tag is misconfigured.

Note: This function was primarily added to facilitate testing in downstream libraries.

func ParseTag

func ParseTag(rt reflect.StructTag) *TagPayload

ParseTag searches for a sensitive tag in the given field's raw tag, parses it, and returns a representational payload. It returns nil if the tag is not found or is misconfigured.

func (TagPayload) Marshal

func (p TagPayload) Marshal() string

Marshal returns back the string representation of the parsed tag.

func (TagPayload) String added in v0.6.0

func (p TagPayload) String() string

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

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