Class: Kotoshu::Integrity::AuditLog

Inherits:
Object
  • Object
show all
Defined in:
lib/kotoshu/integrity/audit_log.rb

Overview

Append-only JSON audit log of every resource download.

Each entry is one JSON object per line, written to audit.log under the configured Kotoshu home directory. The log is consulted by users when investigating "what did Kotoshu fetch?" and by CI for reproducibility audits.

Statuses:

"verified"    — content matched manifest entry's SHA-256
"unverified"  — no manifest entry available; bytes trusted as-is
"mismatch"    — SHA-256 mismatch (also raises IntegrityError)
"missing"     — attempted download failed (network, 404, etc.)

The log is opened, appended, and closed per entry — no long-lived file handle. Writes are line-buffered and fsync'd so the record survives a crash mid-batch.

Rotation

When the current file exceeds rotation_policy.max_bytes, the policy produces a rename plan (see RotationPolicy#plan_for) and the log shifts the existing rotations up by one slot before writing the new entry to a fresh current file. The total on-disk footprint is bounded at +max_bytes * (rotations + 1)+.

Rotation happens under an exclusive flock on a sibling lockfile (+audit.log.lock+) — not on the log itself — because the log path moves during rotation and the lock would otherwise travel with it.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path: self.class.default_path, rotation_policy: nil) ⇒ AuditLog

Returns a new instance of AuditLog.

Parameters:

  • path (String) (defaults to: self.class.default_path)

    Override the log file location.

  • rotation_policy (RotationPolicy, nil) (defaults to: nil)

    When nil, no rotation is performed (the log grows unbounded). Pass a RotationPolicy instance to enable bounded rotation.



50
51
52
53
# File 'lib/kotoshu/integrity/audit_log.rb', line 50

def initialize(path: self.class.default_path, rotation_policy: nil)
  @path = path
  @rotation_policy = rotation_policy
end

Instance Attribute Details

#pathObject (readonly)

Returns the value of attribute path.



44
45
46
# File 'lib/kotoshu/integrity/audit_log.rb', line 44

def path
  @path
end

#rotation_policyObject (readonly)

Returns the value of attribute rotation_policy.



44
45
46
# File 'lib/kotoshu/integrity/audit_log.rb', line 44

def rotation_policy
  @rotation_policy
end

Class Method Details

.default_pathObject

Default location: $XDG_DATA_HOME/kotoshu/audit.log (~/.local/share/kotoshu/audit.log), or $KOTOSHU_AUDIT_LOG.



40
41
42
# File 'lib/kotoshu/integrity/audit_log.rb', line 40

def self.default_path
  Kotoshu::Paths.audit_log_path
end

Instance Method Details

#clear!Object



109
110
111
112
113
114
# File 'lib/kotoshu/integrity/audit_log.rb', line 109

def clear!
  with_exclusive_lock do
    FileUtils.rm_f(@path)
    clear_rotations
  end
end

#each(&block) ⇒ Object

Iterate every recorded entry (parsed Hashes) across the current log and all rotations, newest-first.

Newest-first means: within each file, lines are yielded in reverse write order (the last appended entry first); files are visited in order current, .1, .2, ... (newest file first). This guarantees a strictly decreasing write-time ordering across the whole log, which is what an auditor scanning "what did kotoshu fetch most recently?" expects.



94
95
96
97
98
99
100
101
102
103
# File 'lib/kotoshu/integrity/audit_log.rb', line 94

def each(&block)
  return enum_for(:each) unless block

  each_file_reversed(@path, &block)
  return unless rotation_policy&.rotations&.positive?

  1.upto(rotation_policy.rotations) do |n|
    each_file_reversed("#{@path}.#{n}", &block)
  end
end

#entriesObject



105
106
107
# File 'lib/kotoshu/integrity/audit_log.rb', line 105

def entries
  each.to_a
end

#record(url:, status:, size: nil, sha256: nil, manifest_sha256: nil, resource_id: nil) ⇒ Object

Record one download attempt. Returns the written entry hash.

Parameters:

  • url (String)

    Source URL

  • size (Integer, nil) (defaults to: nil)

    Bytes downloaded (nil on missing)

  • sha256 (String, nil) (defaults to: nil)

    Computed SHA-256 of bytes (nil on missing)

  • manifest_sha256 (String, nil) (defaults to: nil)

    Expected SHA-256 from manifest

  • status (String)

    One of: verified, unverified, mismatch, missing

  • resource_id (String, nil) (defaults to: nil)

    Caller-supplied resource identifier



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/kotoshu/integrity/audit_log.rb', line 63

def record(url:, status:, size: nil, sha256: nil,
           manifest_sha256: nil, resource_id: nil)
  entry = {
    timestamp: Time.now.utc.iso8601,
    url: url,
    resource_id: resource_id,
    size: size,
    sha256: sha256,
    manifest_sha256: manifest_sha256,
    status: status
  }
  FileUtils.mkdir_p(File.dirname(@path))
  with_exclusive_lock do
    rotate_if_needed!
    File.open(@path, "a", encoding: "UTF-8") do |f|
      f.write("#{entry.to_json}\n")
      f.fsync
    end
  end
  entry
end