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.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

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

Returns a new instance of AuditLog.



34
35
36
# File 'lib/kotoshu/integrity/audit_log.rb', line 34

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

Instance Attribute Details

#pathObject (readonly)

Returns the value of attribute path.



32
33
34
# File 'lib/kotoshu/integrity/audit_log.rb', line 32

def path
  @path
end

Class Method Details

.default_pathObject

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



28
29
30
# File 'lib/kotoshu/integrity/audit_log.rb', line 28

def self.default_path
  Kotoshu::Paths.audit_log_path
end

Instance Method Details

#clear!Object



83
84
85
# File 'lib/kotoshu/integrity/audit_log.rb', line 83

def clear!
  FileUtils.rm_f(@path)
end

#eachObject

Iterate every recorded entry (parsed Hashes).



67
68
69
70
71
72
73
74
75
76
77
# File 'lib/kotoshu/integrity/audit_log.rb', line 67

def each
  return enum_for(:each) unless block_given?
  return unless File.exist?(@path)

  File.foreach(@path, encoding: "UTF-8") do |line|
    line = line.strip
    next if line.empty?

    yield JSON.parse(line)
  end
end

#entriesObject



79
80
81
# File 'lib/kotoshu/integrity/audit_log.rb', line 79

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



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/kotoshu/integrity/audit_log.rb', line 46

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))
  File.open(@path, "a", encoding: "UTF-8") do |f|
    f.flock(File::LOCK_EX)
    f.write("#{entry.to_json}\n")
    f.fsync
  end
  entry
end