Module: StillActive::SbomReader

Extended by:
SbomReader
Included in:
SbomReader
Defined in:
lib/still_active/sbom_reader.rb

Overview

Reads a CycloneDX SBOM (e.g. produced by Syft) into a clean, normalized dependency set -- the breadth input for non-Ruby ecosystems, where the maintenance lens runs over deps.dev/ecosyste.ms.

parse returns both the assessable dependencies AND the components it could NOT assess (a real dependency in an unsupported ecosystem, or a library with no PURL -- typically a git/path/local source). Surfacing those is the point: silently dropping them would let an audit report "all clear" while ignoring the deps that are often the riskiest. Genuine non-package noise (GitHub Actions pkg:github, opaque binaries pkg:generic, and file/application components) is excluded, not surfaced.

read keeps the simple "just the dependencies" shape. Never raises -- a missing/malformed SBOM or a bad PURL degrades to empty/skip.

Defined Under Namespace

Classes: Result

Constant Summary collapse

ECOSYSTEMS =

PURL type -> still_active ecosystem (also the deps.dev system, lowercased).

{
  "gem" => :rubygems,
  "npm" => :npm,
  "pypi" => :pypi,
  "cargo" => :cargo,
  "maven" => :maven,
  "golang" => :go,
  "nuget" => :nuget
}.freeze
NOISE_TYPES =

PURL types that are not package dependencies: CI actions and opaque binaries. Excluded from both lists (not "unassessed deps").

["github", "generic"].freeze

Instance Method Summary collapse

Instance Method Details

#parse(path) ⇒ Object



47
48
49
50
51
# File 'lib/still_active/sbom_reader.rb', line 47

def parse(path)
  parse_string(File.read(path))
rescue SystemCallError, IOError
  Result.new(dependencies: [], unassessable: [])
end

#parse_string(body) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/still_active/sbom_reader.rb', line 53

def parse_string(body)
  doc = JSON.parse(body)
  components = doc.is_a?(Hash) ? doc["components"] : nil
  return Result.new(dependencies: [], unassessable: []) unless components.is_a?(Array)

  # Only trust the prod/dev split when the generator actually marks it
  # somewhere (scope, or a dev property). syft-style SBOMs mark nothing, so an
  # unmarked component is genuinely unknown -- never assume it's production.
  # Scoped to library components: scope/properties are legal on any component
  # type (application/file/...), and a dev signal on one of those is unrelated
  # to whether the dependency library components are marked.
  marks_dev = components.any? { |c| c.is_a?(Hash) && c["type"] == "library" && dev_signal?(c) }

  deps = []
  unassessable = []
  entries_by_ref = {}
  components.each do |component|
    kind, entry = classify(component)
    entry[:production] = !dev_signal?(component) if kind == :dependency && marks_dev
    if kind == :dependency
      ref = component["bom-ref"]
      entries_by_ref[ref] = entry if ref.is_a?(String)
      deps << entry
    end
    unassessable << entry if kind == :unassessable
  end
  drop_project_components(doc, deps, entries_by_ref)
  attach_dependency_graph(doc, deps, entries_by_ref)
  # uniq collapses a package that a generator lists more than once (e.g. Syft's
  # per-location entries); `production` is derived from each component's own
  # signal, so duplicates of the same name+version dedup cleanly unless a
  # generator marks the copies inconsistently (malformed; not observed).
  Result.new(dependencies: deps.uniq, unassessable: unassessable.uniq)
rescue JSON::ParserError
  Result.new(dependencies: [], unassessable: [])
end

#read(path) ⇒ Object



44
# File 'lib/still_active/sbom_reader.rb', line 44

def read(path) = parse(path).dependencies

#read_string(body) ⇒ Object



45
# File 'lib/still_active/sbom_reader.rb', line 45

def read_string(body) = parse_string(body).dependencies