Line data Source code
1 : // Package util は、共通の軽量ユーティリティ関数群を提供します。
2 : package util
3 :
4 : import (
5 : "errors"
6 : "fmt"
7 : "os"
8 : "regexp"
9 : "strings"
10 :
11 : "gopkg.in/yaml.v3"
12 : )
13 :
14 : // 機微情報マスキングユーティリティ
15 : //
16 : // このパッケージは、リクエストやレスポンスなどの構造化ログ内に含まれる
17 : // パスワード・トークン・個人情報等を安全にマスクするための共通ユーティリティを提供する。
18 : // YAML 設定ファイルからマスク対象キーを読み込み、キー名に応じて完全マスク・部分マスクを自動適用する。
19 :
20 : // SensitiveConfig は YAML から読み込まれるマスク設定情報を表す構造体。
21 : type SensitiveConfig struct {
22 : Sensitive struct {
23 : Full []string `yaml:"full"` // 完全マスク対象キーの一覧
24 : Part []string `yaml:"part"` // 部分マスク対象キーの一覧
25 : } `yaml:"sensitive"`
26 : Partial struct {
27 : KeepHead int `yaml:"keep_head"` // 部分マスク時に残す先頭文字数
28 : KeepTail int `yaml:"keep_tail"` // 部分マスク時に残す末尾文字数
29 : MaskChar string `yaml:"mask_char"` // マスクに使用する文字
30 : } `yaml:"partial"`
31 : }
32 :
33 : // Masker は機微情報マスカーの抽象インタフェース。
34 : // 具体実装(keyMatcher)は外部に公開しない。
35 : type Masker interface {
36 : MaskByKey(key string, val any) any
37 : MaskMapShallow(src map[string]any) map[string]any
38 : MaskAnyRecursive(key string, v any) any
39 : }
40 :
41 : // NewMasker は設定から Masker を生成するファサード。
42 : // 具体型 keyMatcher を隠蔽したい場合はこの関数を使う。
43 0 : func NewMasker(cfg *SensitiveConfig) (Masker, error) {
44 0 : return NewKeyMatcher(cfg)
45 0 : }
46 :
47 : // LoadSensitiveConfig は YAML ファイルからマスク設定を読み込む。
48 : // ファイルが存在しない場合やパースに失敗した場合はエラーを返す。
49 0 : func LoadSensitiveConfig(path string) (*SensitiveConfig, error) {
50 0 : b, err := os.ReadFile(path)
51 0 : if err != nil {
52 0 : return nil, fmt.Errorf("read config: %w", err)
53 0 : }
54 :
55 0 : var c SensitiveConfig
56 0 : if err := yaml.Unmarshal(b, &c); err != nil {
57 0 : return nil, fmt.Errorf("unmarshal yaml: %w", err)
58 0 : }
59 :
60 : // デフォルト値を補完
61 0 : if c.Partial.KeepHead < 0 {
62 0 : c.Partial.KeepHead = 1
63 0 : }
64 0 : if c.Partial.KeepTail < 0 {
65 0 : c.Partial.KeepTail = 4
66 0 : }
67 0 : if c.Partial.MaskChar == "" {
68 0 : c.Partial.MaskChar = "*"
69 0 : }
70 :
71 0 : return &c, nil
72 : }
73 :
74 : // Redacted は完全マスク時に使用される固定文字列。
75 : const Redacted = "[REDACTED]"
76 :
77 : // FullMask は渡された値を完全にマスクし、固定文字列 [REDACTED] を返す。
78 0 : func FullMask(_ any) string {
79 0 : return Redacted
80 0 : }
81 :
82 : // PartialMask は渡された文字列を部分的にマスクする。
83 : // 先頭と末尾を残し、中間を指定のマスク文字で埋める。
84 0 : func PartialMask(s string, keepHead, keepTail int, maskChar string) string {
85 0 : if s == "" {
86 0 : return s
87 0 : }
88 0 : if keepHead < 0 {
89 0 : keepHead = 0
90 0 : }
91 0 : if keepTail < 0 {
92 0 : keepTail = 0
93 0 : }
94 0 : if maskChar == "" {
95 0 : maskChar = "*"
96 0 : }
97 :
98 0 : runes := []rune(s)
99 0 : n := len(runes)
100 0 : if keepHead+keepTail >= n {
101 0 : // 残す長さが総文字数以上なら、そのまま返す
102 0 : return s
103 0 : }
104 :
105 0 : head := string(runes[:keepHead])
106 0 : tail := string(runes[n-keepTail:])
107 0 : midLen := n - keepHead - keepTail
108 0 :
109 0 : return head + strings.Repeat(maskChar, midLen) + tail
110 : }
111 :
112 : // keyMatcher はキー名に対するマスク種別(完全/部分)判定を行う構造体。
113 : type keyMatcher struct {
114 : full []*regexp.Regexp // 完全マスク対象の正規表現リスト
115 : part []*regexp.Regexp // 部分マスク対象の正規表現リスト
116 :
117 : keepHead int
118 : keepTail int
119 : maskChar string
120 : }
121 :
122 : // globToRegex はワイルドカード(*)を含むキー定義を正規表現に変換する。
123 : // 大文字小文字は区別しない。
124 0 : func globToRegex(glob string) (*regexp.Regexp, error) {
125 0 : p := regexp.QuoteMeta(glob)
126 0 : p = strings.ReplaceAll(p, "\\*", ".*")
127 0 : p = "(?i)^.*" + p + ".*$" // キー名部分一致許可
128 0 : return regexp.Compile(p)
129 0 : }
130 :
131 : // NewKeyMatcher は SensitiveConfig から keyMatcher を生成する。
132 : // Full/Part のパターンを正規表現化して保持する。
133 0 : func NewKeyMatcher(cfg *SensitiveConfig) (*keyMatcher, error) {
134 0 : if cfg == nil {
135 0 : return nil, errors.New("nil config")
136 0 : }
137 :
138 0 : m := &keyMatcher{
139 0 : keepHead: cfg.Partial.KeepHead,
140 0 : keepTail: cfg.Partial.KeepTail,
141 0 : maskChar: cfg.Partial.MaskChar,
142 0 : }
143 0 :
144 0 : // 完全マスクパターンをコンパイル
145 0 : for _, g := range cfg.Sensitive.Full {
146 0 : re, err := globToRegex(g)
147 0 : if err != nil {
148 0 : return nil, fmt.Errorf("compile full pattern %q: %w", g, err)
149 0 : }
150 0 : m.full = append(m.full, re)
151 : }
152 :
153 : // 部分マスクパターンをコンパイル
154 0 : for _, g := range cfg.Sensitive.Part {
155 0 : re, err := globToRegex(g)
156 0 : if err != nil {
157 0 : return nil, fmt.Errorf("compile part pattern %q: %w", g, err)
158 0 : }
159 0 : m.part = append(m.part, re)
160 : }
161 :
162 0 : return m, nil
163 : }
164 :
165 : // isFullKey は与えられたキーが完全マスク対象に一致するかを判定する。
166 0 : func (m *keyMatcher) isFullKey(key string) bool {
167 0 : for _, re := range m.full {
168 0 : if re.MatchString(key) {
169 0 : return true
170 0 : }
171 : }
172 0 : return false
173 : }
174 :
175 : // isPartKey は与えられたキーが部分マスク対象に一致するかを判定する。
176 0 : func (m *keyMatcher) isPartKey(key string) bool {
177 0 : for _, re := range m.part {
178 0 : if re.MatchString(key) {
179 0 : return true
180 0 : }
181 : }
182 0 : return false
183 : }
184 :
185 : // MaskByKey はキー名に応じて値を完全または部分的にマスクする。
186 : // 対象外のキーはそのまま返す。
187 0 : func (m *keyMatcher) MaskByKey(key string, val any) any {
188 0 : if key == "" {
189 0 : return val
190 0 : }
191 0 : if m.isFullKey(key) {
192 0 : return FullMask(val)
193 0 : }
194 0 : if m.isPartKey(key) {
195 0 : s := fmt.Sprintf("%v", val)
196 0 : return PartialMask(s, m.keepHead, m.keepTail, m.maskChar)
197 0 : }
198 0 : return val
199 : }
200 :
201 : // MaskMapShallow は map[string]any を対象に、
202 : // 各キーに応じてマスク処理を適用する(1階層のみ)。
203 0 : func (m *keyMatcher) MaskMapShallow(src map[string]any) map[string]any {
204 0 : if src == nil {
205 0 : return nil
206 0 : }
207 :
208 0 : dst := make(map[string]any, len(src))
209 0 : for k, v := range src {
210 0 : dst[k] = m.MaskByKey(k, v)
211 0 : }
212 0 : return dst
213 : }
214 :
215 : // MaskAnyRecursive は任意の構造(map やスライスを含む)に対して
216 : // 再帰的にマスク処理を適用する。
217 : // JSON デコード結果などネストした構造体を安全にマスクしたい場合に使用する。
218 0 : func (m *keyMatcher) MaskAnyRecursive(key string, v any) any {
219 0 : switch t := v.(type) {
220 0 : case map[string]any:
221 0 : out := make(map[string]any, len(t))
222 0 : for k, vv := range t {
223 0 : out[k] = m.MaskAnyRecursive(k, vv)
224 0 : }
225 0 : return out
226 0 : case []any:
227 0 : out := make([]any, len(t))
228 0 : for i, vv := range t {
229 0 : out[i] = m.MaskAnyRecursive(key, vv)
230 0 : }
231 0 : return out
232 0 : default:
233 0 : return m.MaskByKey(key, v)
234 : }
235 : }
|