Fix the five library adoption review findings
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package diagnostics
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -9,12 +8,14 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/chunking"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/normalization"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/redaction"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/reporting"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
|
||||
)
|
||||
|
||||
// RunDirectory represents a per-run diagnostics directory
|
||||
type RunDirectory struct {
|
||||
redactor redaction.Redactor
|
||||
path string
|
||||
retention string
|
||||
createdAt time.Time
|
||||
@@ -63,7 +64,7 @@ type InvocationMetadata struct {
|
||||
}
|
||||
|
||||
// NewRunDirectory creates a new run directory under the configured work dir
|
||||
func NewRunDirectory(workDir, retention string) (*RunDirectory, error) {
|
||||
func NewRunDirectory(workDir, retention string, secrets ...string) (*RunDirectory, error) {
|
||||
if workDir == "" {
|
||||
workDir = ".audita-runs"
|
||||
}
|
||||
@@ -83,6 +84,7 @@ func NewRunDirectory(workDir, retention string) (*RunDirectory, error) {
|
||||
|
||||
return &RunDirectory{
|
||||
path: runPath,
|
||||
redactor: redaction.New(secrets),
|
||||
retention: retention,
|
||||
createdAt: time.Now().UTC(),
|
||||
}, nil
|
||||
@@ -107,7 +109,7 @@ func (r *RunDirectory) WriteInvocationMetadata(metadata InvocationMetadata) erro
|
||||
}
|
||||
|
||||
path := filepath.Join(r.path, ArtifactInvocationMetadata)
|
||||
bytes, err := json.MarshalIndent(metadata, "", " ")
|
||||
bytes, err := r.redactor.JSON(metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal invocation metadata: %w", err)
|
||||
}
|
||||
@@ -120,11 +122,7 @@ func (r *RunDirectory) WriteInvocationMetadata(metadata InvocationMetadata) erro
|
||||
|
||||
// WriteEffectiveConfig writes redacted effective config metadata for this run.
|
||||
func (r *RunDirectory) WriteEffectiveConfig(effectiveYAML []byte) error {
|
||||
path := filepath.Join(r.path, ArtifactEffectiveConfig)
|
||||
if err := os.WriteFile(path, effectiveYAML, 0o644); err != nil {
|
||||
return fmt.Errorf("failed to write effective config: %w", err)
|
||||
}
|
||||
return nil
|
||||
return r.WriteYAMLArtifact(ArtifactEffectiveConfig, effectiveYAML)
|
||||
}
|
||||
|
||||
// WriteSourceTranscript writes the source transcript artifact
|
||||
@@ -137,7 +135,7 @@ func (r *RunDirectory) WriteSourceTranscript(transcript *schema.SourceTranscript
|
||||
|
||||
// Write parsed source for debugging
|
||||
parsedPath := filepath.Join(r.path, ArtifactParsedSourceTranscript)
|
||||
parsedBytes, err := json.MarshalIndent(transcript, "", " ")
|
||||
parsedBytes, err := r.redactor.JSON(transcript)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal parsed source transcript: %w", err)
|
||||
}
|
||||
@@ -165,7 +163,7 @@ func (r *RunDirectory) WriteNormalizedTranscript(transcript *schema.Transcript)
|
||||
// WriteNormalizationSummary writes the normalization summary artifact
|
||||
func (r *RunDirectory) WriteNormalizationSummary(summary *normalization.NormalizationSummary) error {
|
||||
summaryPath := filepath.Join(r.path, ArtifactNormalizationSummary)
|
||||
bytes, err := json.MarshalIndent(summary, "", " ")
|
||||
bytes, err := r.redactor.JSON(summary)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal normalization summary: %w", err)
|
||||
}
|
||||
@@ -179,7 +177,7 @@ func (r *RunDirectory) WriteNormalizationSummary(summary *normalization.Normaliz
|
||||
// WriteReport writes the authoritative report artifact
|
||||
func (r *RunDirectory) WriteReport(report reporting.ProcessReport) error {
|
||||
reportPath := filepath.Join(r.path, ArtifactReport)
|
||||
bytes, err := json.MarshalIndent(report, "", " ")
|
||||
bytes, err := r.redactor.JSON(report)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal report: %w", err)
|
||||
}
|
||||
@@ -193,13 +191,13 @@ func (r *RunDirectory) WriteReport(report reporting.ProcessReport) error {
|
||||
// WriteErrorLog writes an error log on failure
|
||||
func (r *RunDirectory) WriteErrorLog(errorMessage string) error {
|
||||
errorPath := filepath.Join(r.path, ArtifactErrorLog)
|
||||
return os.WriteFile(errorPath, []byte(errorMessage+"\n"), 0o644)
|
||||
return os.WriteFile(errorPath, []byte(r.redactor.String(errorMessage)+"\n"), 0o644)
|
||||
}
|
||||
|
||||
// WriteChunkingSummary writes the chunking summary artifact
|
||||
func (r *RunDirectory) WriteChunkingSummary(summary *chunking.DetailedSummary) error {
|
||||
summaryPath := filepath.Join(r.path, ArtifactChunkingSummary)
|
||||
bytes, err := json.MarshalIndent(summary, "", " ")
|
||||
bytes, err := r.redactor.JSON(summary)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal chunking summary: %w", err)
|
||||
}
|
||||
@@ -212,7 +210,7 @@ func (r *RunDirectory) WriteChunkingSummary(summary *chunking.DetailedSummary) e
|
||||
|
||||
func (r *RunDirectory) WriteJSONArtifact(name string, payload any) error {
|
||||
artifactPath := filepath.Join(r.path, name)
|
||||
bytes, err := json.MarshalIndent(payload, "", " ")
|
||||
bytes, err := r.redactor.JSON(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal %s: %w", name, err)
|
||||
}
|
||||
@@ -223,7 +221,13 @@ func (r *RunDirectory) WriteJSONArtifact(name string, payload any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RunDirectory) WriteArtifact(name string, payload []byte) error {
|
||||
// WriteYAMLArtifact writes a sanitized YAML diagnostic artifact.
|
||||
func (r *RunDirectory) WriteYAMLArtifact(name string, payload []byte) error {
|
||||
var err error
|
||||
payload, err = r.redactor.YAML(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(r.path, name), payload, 0o644); err != nil {
|
||||
return fmt.Errorf("failed to write %s: %w", name, err)
|
||||
}
|
||||
|
||||
123
internal/core/redaction/redaction.go
Normal file
123
internal/core/redaction/redaction.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// Package redaction sanitizes diagnostic representations without modifying domain data.
|
||||
package redaction
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Redactor replaces configured values in a single pass, longest matches first.
|
||||
// Its zero value is usable and leaves strings unchanged.
|
||||
type Redactor struct{ replacer *strings.Replacer }
|
||||
|
||||
func New(secrets []string) Redactor {
|
||||
values := map[string]bool{}
|
||||
for _, secret := range secrets {
|
||||
if secret == "" {
|
||||
continue
|
||||
}
|
||||
values[secret] = true
|
||||
// Provider content may itself be serialized JSON inside a string field.
|
||||
encoded, _ := json.Marshal(secret)
|
||||
values[string(encoded[1:len(encoded)-1])] = true
|
||||
}
|
||||
keys := make([]string, 0, len(values))
|
||||
for value := range values {
|
||||
keys = append(keys, value)
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
if len(keys[i]) == len(keys[j]) {
|
||||
return keys[i] < keys[j]
|
||||
}
|
||||
return len(keys[i]) > len(keys[j])
|
||||
})
|
||||
pairs := make([]string, 0, 2*len(keys))
|
||||
for _, key := range keys {
|
||||
pairs = append(pairs, key, "[REDACTED]")
|
||||
}
|
||||
return Redactor{replacer: strings.NewReplacer(pairs...)}
|
||||
}
|
||||
|
||||
func (r Redactor) String(value string) string {
|
||||
if r.replacer == nil {
|
||||
return value
|
||||
}
|
||||
return r.replacer.Replace(value)
|
||||
}
|
||||
|
||||
// JSON serializes first, then redacts only decoded string values and keys.
|
||||
// Numbers retain their exact representation. Failure never returns original data.
|
||||
func (r Redactor) JSON(value any) ([]byte, error) {
|
||||
raw, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("serialize diagnostic: %s", r.String(err.Error()))
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(raw))
|
||||
decoder.UseNumber()
|
||||
var decoded any
|
||||
if err := decoder.Decode(&decoded); err != nil {
|
||||
return nil, fmt.Errorf("decode diagnostic: %s", r.String(err.Error()))
|
||||
}
|
||||
safe, err := r.walk(decoded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.MarshalIndent(safe, "", " ")
|
||||
}
|
||||
|
||||
func (r Redactor) walk(value any) (any, error) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return r.String(v), nil
|
||||
case []any:
|
||||
for i := range v {
|
||||
var err error
|
||||
v[i], err = r.walk(v[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
case map[string]any:
|
||||
safe := make(map[string]any, len(v))
|
||||
for key, item := range v {
|
||||
safeKey := r.String(key)
|
||||
if _, exists := safe[safeKey]; exists {
|
||||
return nil, fmt.Errorf("redaction would produce duplicate object keys")
|
||||
}
|
||||
safeItem, err := r.walk(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
safe[safeKey] = safeItem
|
||||
}
|
||||
return safe, nil
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// YAML preserves scalar types while sanitizing string nodes, including map keys.
|
||||
func (r Redactor) YAML(raw []byte) ([]byte, error) {
|
||||
var node yaml.Node
|
||||
if err := yaml.Unmarshal(raw, &node); err != nil {
|
||||
return nil, fmt.Errorf("decode diagnostic YAML: %s", r.String(err.Error()))
|
||||
}
|
||||
var visit func(*yaml.Node)
|
||||
visit = func(n *yaml.Node) {
|
||||
if n.Kind == yaml.ScalarNode && n.Tag == "!!str" {
|
||||
n.Value = r.String(n.Value)
|
||||
}
|
||||
n.HeadComment = r.String(n.HeadComment)
|
||||
n.LineComment = r.String(n.LineComment)
|
||||
n.FootComment = r.String(n.FootComment)
|
||||
for _, child := range n.Content {
|
||||
visit(child)
|
||||
}
|
||||
}
|
||||
visit(&node)
|
||||
return yaml.Marshal(&node)
|
||||
}
|
||||
92
internal/core/redaction/redaction_test.go
Normal file
92
internal/core/redaction/redaction_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package redaction_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/diagnostics"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/redaction"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/reporting"
|
||||
)
|
||||
|
||||
func TestStringRedactionPreservesSerializationAndDomainData(t *testing.T) {
|
||||
secrets := []string{"2", `quoted"\key`, "overlap", "overlap-long", "REDACTED"}
|
||||
r := redaction.New(secrets)
|
||||
original := map[string]any{"number": 2, "large": json.Number("9007199254740993"), "bool": true, "text": `echo:2 quoted"\key overlap-long overlap`}
|
||||
raw, err := r.JSON(original)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal(raw, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if decoded["number"] != float64(2) || decoded["bool"] != true || !strings.Contains(string(raw), "9007199254740993") {
|
||||
t.Fatalf("changed scalar meanings: %s", raw)
|
||||
}
|
||||
if decoded["text"] != "echo:[REDACTED] [REDACTED] [REDACTED] [REDACTED]" {
|
||||
t.Fatalf("redaction: %s", raw)
|
||||
}
|
||||
if original["text"] != `echo:2 quoted"\key overlap-long overlap` {
|
||||
t.Fatal("mutated domain data")
|
||||
}
|
||||
if raw, err := r.JSON(math.NaN()); err == nil || raw != nil {
|
||||
t.Fatalf("unsafe fallback: %s %v", raw, err)
|
||||
}
|
||||
if raw, err := r.JSON(map[string]string{"overlap": "first", "overlap-long": "second"}); err == nil || raw != nil {
|
||||
t.Fatalf("colliding redacted keys: %s %v", raw, err)
|
||||
}
|
||||
nested, _ := json.Marshal(map[string]string{"reason": `quoted"\key`})
|
||||
raw, err = r.JSON(map[string]string{"final_content": string(nested)})
|
||||
if err != nil || strings.Contains(string(raw), "key") {
|
||||
t.Fatalf("nested content: %s %v", raw, err)
|
||||
}
|
||||
yaml, err := r.YAML([]byte("version: 2\nenabled: true\nmessage: echo:2\n"))
|
||||
if err != nil || !strings.Contains(string(yaml), "version: 2") || !strings.Contains(string(yaml), "echo:[REDACTED]") {
|
||||
t.Fatalf("YAML: %s %v", yaml, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportWritersRedactStringsAndFailClosed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
version := 2
|
||||
report := reporting.ProcessReport{ErrorMessage: "echo:2"}
|
||||
report.ReportMetadata.ConfigVersion = &version
|
||||
run, err := diagnostics.NewRunDirectory(dir, "always", "2")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
external := filepath.Join(dir, "external.json")
|
||||
if err := reporting.WriteProcessReport(external, report, "2"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := run.WriteReport(report); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, path := range []string{external, filepath.Join(run.Path(), diagnostics.ArtifactReport)} {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got reporting.ProcessReport
|
||||
if err := json.Unmarshal(raw, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.ErrorMessage != "echo:[REDACTED]" || got.ReportMetadata.ConfigVersion == nil || *got.ReportMetadata.ConfigVersion != 2 {
|
||||
t.Fatalf("report: %+v", got)
|
||||
}
|
||||
}
|
||||
if report.ErrorMessage != "echo:2" {
|
||||
t.Fatal("mutated input report")
|
||||
}
|
||||
if err := run.WriteJSONArtifact("invalid.json", map[string]any{"secret": "echo:2", "invalid": math.NaN()}); err == nil {
|
||||
t.Fatal("expected serialization failure")
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(run.Path(), "invalid.json")); !os.IsNotExist(err) {
|
||||
t.Fatalf("failed artifact persisted: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package reporting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/redaction"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
||||
stagewarnings "gitea.maximumdirect.net/eric/audita/internal/framework/warnings"
|
||||
)
|
||||
@@ -124,8 +124,8 @@ type ChunkingSummary struct {
|
||||
MinSectionTokens int `json:"min_section_tokens"`
|
||||
}
|
||||
|
||||
func WriteProcessReport(path string, report ProcessReport) error {
|
||||
payload, err := json.MarshalIndent(report, "", " ")
|
||||
func WriteProcessReport(path string, report ProcessReport, secrets ...string) error {
|
||||
payload, err := redaction.New(secrets).JSON(report)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal process report JSON: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user