Skip to content

[TC-3272] Convert gensbom to TypeScript, add several security checks#2334

Draft
i386x wants to merge 8 commits intoguacsec:mainfrom
i386x:gensbom-secure
Draft

[TC-3272] Convert gensbom to TypeScript, add several security checks#2334
i386x wants to merge 8 commits intoguacsec:mainfrom
i386x:gensbom-secure

Conversation

@i386x
Copy link
Copy Markdown
Contributor

@i386x i386x commented Apr 16, 2026

gensbom was converted to TypeScript from Bash using Claude Code as the first step, then it was manually refactored and split into several files to reflect a modern TypeScript project structure. Security checks requested by TC-3272 were also added.

Summary by Sourcery

Convert the gensbom SBOM generation tool from a Bash script into a TypeScript-based Node.js CLI and container image, adding stricter configuration validation and TPA integration while introducing a modern build, lint, and formatting setup.

New Features:

  • Introduce a TypeScript implementation of the gensbom CLI that generates SBOMs with Syft, ingests them into TPA, and produces a ZIP archive of the results.
  • Add a structured, colorized help/usage formatter for the gensbom CLI.
  • Provide a Node.js entrypoint script to run gensbom as an installed binary.

Enhancements:

  • Add security validations for Docker registry credentials, including enforcement of Quay.io robot-account tokens and rejection of unsupported registries.
  • Improve error handling, logging, and colored output around Syft execution, TPA communication, and file handling.
  • Refine the gensbom README formatting and add a dedicated CLAUDE.md to document tool behavior and usage for AI-assisted development.

Build:

  • Replace the UBI base image with Node.js-based builder and runtime images, running TypeScript build, lint, and format checks during the image build.
  • Introduce a Node/TypeScript project configuration for gensbom, including package.json, package-lock.json, tsconfig.json, ESLint config, and Prettier config.

i386x added 8 commits April 16, 2026 03:16
Related: TC-3272
Assisted-by: Claude Code
Signed-off-by: Jiří Kučera <jkucera@redhat.com>
Fix `tsconfig.json` and `package.json` to reflect modern NodeJS
(for NodeJS >= 20) project settings and layout.

Add settings for linters.

Signed-off-by: Jiří Kučera <jkucera@redhat.com>
Migrate to `src/` and `dist/` project layout. Split project to
more files.

Add requested security checks (TC-3272).

Do not fail when `TPA_SERVICE_URL` is not set, just generate SBOMs
with Syft and do not ingest them to TPA.

Signed-off-by: Jiří Kučera <jkucera@redhat.com>
Signed-off-by: Jiří Kučera <jkucera@redhat.com>
Signed-off-by: Jiří Kučera <jkucera@redhat.com>
Signed-off-by: Jiří Kučera <jkucera@redhat.com>
Signed-off-by: Jiří Kučera <jkucera@redhat.com>
Also fix replacing all the characters.

Signed-off-by: Jiří Kučera <jkucera@redhat.com>
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Apr 16, 2026

Reviewer's Guide

This PR replaces the previous Bash-based gensbom.sh implementation with a modular TypeScript CLI and container image, adds strong configuration and registry-credential security validation, and wires the tool into a Node-based build/lint/test toolchain and runtime container image.

Sequence diagram for gensbom CLI execution and security checks

sequenceDiagram
    actor User
    participant CLI as gensbom_bin
    participant Main as main
    participant Config as Config
    participant Gen as SbomGenerator
    participant TPA as TPAService
    participant Syft as syft_binary
    participant TPAHTTP as TPA_HTTP_API

    User->>CLI: invoke gensbom images.txt
    CLI->>Main: main()

    Main->>Config: new Config(env)
    Main->>Config: validate()
    Config->>Config: checkTpaServiceUrl()
    Config->>Config: validateDockerConfig()
    Config-->>Config: parse config.json
    Config-->>Config: validate quay.io robot credentials
    Config->>Config: checkTpaAuthToken()
    Config->>Config: checkTrustCert()

    Main->>Gen: new SbomGenerator(config)
    Gen->>TPA: new TPAService(config)

    Main->>Gen: generate("images.txt")
    Gen->>TPA: ping()
    TPA->>TPAHTTP: GET /api/v2/sbom?limit=1
    TPAHTTP-->>TPA: response or error

    loop for each image in file
      Gen->>Syft: exec syft scan image -o cyclonedx-json
      Syft-->>Gen: SBOM file path or failure
      Gen->>TPA: ingest(sbomPath)
      alt SBOM valid and TPA configured
        TPA->>TPAHTTP: POST /api/v2/sbom
        TPAHTTP-->>TPA: 2xx/4xx/5xx
      else
        TPA-->>Gen: log warning
      end
    end

    Gen->>Gen: pack() create sboms.zip

    Gen-->>Main: resolve
    Main-->>CLI: return exit code
Loading

Class diagram for the new TypeScript gensbom core

classDiagram
    direction LR

    class FatalError {
      +number errorCode
      +constructor(message: string, errorCode: number)
    }

    class Config {
      +string shell
      +string tpaServiceUrl
      +string dockerConfig
      +string tpaAuthToken
      +string trustCert
      +constructor(environ: ProcessEnv)
      +validate() void
      +checkTpaServiceUrl() void
      +validateDockerConfig() void
      +checkTpaAuthToken() void
      +checkTrustCert() void
    }

    class ConfigError {
      +constructor(cfgfile: string, message: string)
    }

    class InsecureConfigError {
      +constructor(cfgfile: string, message: string)
    }

    class UnsupportedOCIRegistryError {
      +constructor(cfgfile: string, registry: string)
    }

    class TPAService {
      +string baseUrl
      +AxiosInstance service
      +constructor(config: Config)
      +ping() Promise~void~
      +ingest(sbom: string) Promise~void~
    }

    class SbomGenerator {
      -string shell
      -TPAService tpaService
      -boolean prepared
      -number imageCount
      -string sbomsDir
      -string sbomsArchive
      +constructor(config: Config)
      +prepare() void
      +sbomFileFromImageURI(image: string) string
      +runSyft(image: string) string
      +pack() Promise~void~
      +generate(inputFile: string) Promise~void~
    }

    class HelpFormatter {
      -Formatter[] lines
      +constructor()
      +blankLine() void
      +usage(content: string) HelpFormatter
      +par(content: string) HelpFormatter
      +codeblock(content: CodeBlockItem) HelpFormatter
      +warning(content: string) HelpFormatter
      +section(content: string) HelpFormatter
      +term(name: string, description: string) HelpFormatter
      +format(style: Style) string
    }

    class Chunk {
      #string content
      +constructor(content: string)
    }

    class Comment {
      +format(style: Style) string
    }

    class Code {
      +format(style: Style) string
    }

    class Usage {
      +constructor(content: string)
    }

    class Term {
      +format(style: Style) string
    }

    class QuotedTerm {
      +format(style: Style) string
    }

    class Warning {
      +constructor()
      +format(style: Style) string
    }

    class Section {
      +format(style: Style) string
    }

    class Line {
      -Fragment[] fragments
      +constructor(fragments: Fragment)
      +format(style: Style) string
    }

    class Utils {
      <<utility>>
      +scriptName() string
      +stringify(obj: object, indent: string) string
      +inform(messages: string) void
      +warning(messages: string) void
      +werror(message: string, color: Color) void
      +handleError(err: unknown) number
      +workspace() string
      +isFile(path: string) boolean
      +isNonEmptyFile(path: string) boolean
    }

    class MainModule {
      <<module>>
      +main() Promise~number~
    }

    FatalError <|-- InsecureConfigError
    InsecureConfigError <|-- UnsupportedOCIRegistryError

    Error <|-- FatalError
    Error <|-- ConfigError

    Config ..> Utils : uses
    Config ..> FatalError : throws

    TPAService ..> Config : reads
    TPAService ..> Utils : logging

    SbomGenerator ..> Config : uses
    SbomGenerator ..> TPAService : aggregates
    SbomGenerator ..> Utils : fileHelpers

    HelpFormatter o-- Line
    Line o-- Chunk
    Chunk <|-- Comment
    Chunk <|-- Code
    Code <|-- Usage
    Chunk <|-- Term
    Term <|-- QuotedTerm
    Chunk <|-- Warning
    Chunk <|-- Section

    MainModule ..> Config
    MainModule ..> SbomGenerator
    MainModule ..> Utils : handleError
Loading

File-Level Changes

Change Details Files
Replace Bash script with a TypeScript-based CLI that generates SBOMs, uploads them to TPA, and packages results.
  • Introduce a TypeScript entrypoint that reads an image list file, validates arguments, and orchestrates SBOM generation and ingestion
  • Implement an SbomGenerator class to invoke Syft, validate image URIs, manage the sboms directory, and zip non-empty SBOM JSON files
  • Execute Syft via child_process with shell selection from configuration and ensure corrupted/empty files are removed before archiving
  • Provide a Node-based CLI wrapper (bin/gensbom.js) that calls the compiled main() and handles fatal errors
etc/gensbom/src/index.ts
etc/gensbom/bin/gensbom.js
Add configuration management with strict security checks for Docker registry credentials and TPA connectivity.
  • Introduce a Config class that reads environment variables, resolves workspace-relative paths, and validates required settings
  • Add Docker config (config.json) parsing with per-registry validation, currently supporting only quay.io with robot-account-only enforcement
  • Emit warnings when Docker config is missing/invalid, when TPA service URL or auth token are unset, and when optional trust.crt is absent
  • Define custom error types for fatal and insecure configuration states and centralize error handling for consistent exit codes and messaging
etc/gensbom/src/config.ts
etc/gensbom/src/utils.ts
Implement a TPAService client with HTTPS, optional custom CA, and detailed request/response logging.
  • Create an Axios-based TPAService wrapper that configures base URL, timeout, Authorization header, and optional HTTPS CA agent from Config
  • Add ping() to test connectivity by querying /api/v2/sbom and log responses or errors without failing the main flow
  • Add ingest() to POST SBOM JSON files to TPA, with graceful handling and warnings on failures
  • Log structured, colorized HTTP responses and errors to aid debugging while not aborting processing of other images
etc/gensbom/src/tpa.ts
etc/gensbom/src/utils.ts
Provide a rich, colorized CLI help/usage system with structured formatting utilities.
  • Implement a HelpFormatter with support for sections, terms, warnings, code blocks, and automatic script-name substitution
  • Add formatting primitives (Comment, Code, Term, QuotedTerm, Warning, Section, Line) with ANSI color support via node:util.styleText
  • Parse inline term markers ( and code) into styled fragments for consistent help text highlighting
  • Wire a usage() function using HelpFormatter that documents container usage, files, env vars, and references
etc/gensbom/src/format.ts
etc/gensbom/src/usage.ts
etc/gensbom/src/utils.ts
Convert the container build to a multi-stage Node.js image that builds and runs the TypeScript implementation instead of the Bash script.
  • Switch the base image to ubi9/nodejs-22 for the build stage and ubi9/nodejs-22-minimal for runtime, dropping the generic UBI base
  • Run npm ci, format check, build, and lint in the builder stage as part of the image build pipeline
  • Copy compiled dist, bin scripts, and package.json into the runtime image and perform a production-only npm install
  • Change the container entrypoint to the Node CLI (bin/gensbom.js) and remove the legacy gensbom.sh script and gawk/zip dependencies
etc/gensbom/Containerfile
etc/gensbom/gensbom.sh
Set up a modern TypeScript/Node project structure with linting, formatting, and Jest testing scaffolding.
  • Add package.json with build, lint, format, update, and test scripts plus runtime and dev dependencies (axios, archiver, eslint, ts, jest, etc.)
  • Introduce a strict tsconfig.json enabling strict type-checking, modern ES2022 target, and Node ESM module settings
  • Add an ESLint flat config with extensive best-practice and stylistic rules, plus @Stylistic integration and projectService for TS
  • Include supporting project files such as package-lock, .prettierrc.json, and .gitignore for node_modules/dist artifacts
etc/gensbom/package.json
etc/gensbom/tsconfig.json
etc/gensbom/eslint.config.mjs
etc/gensbom/.gitignore
etc/gensbom/.prettierrc.json
etc/gensbom/package-lock.json
Update documentation to describe the new TypeScript implementation and align markdown formatting.
  • Adjust README.md bullet formatting to use consistent dash-based lists for prerequisites, outputs, and references
  • Document both container-based and script-based usage outputs consistently with the new implementation behavior
  • Add CLAUDE.md with high-level description, usage examples for Bash and TypeScript versions, environment variables, authentication files, build args, and behavior overview for AI-assisted code edits
etc/gensbom/README.md
etc/gensbom/CLAUDE.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@ctron
Copy link
Copy Markdown
Contributor

ctron commented Apr 16, 2026

So if we're converting this away from Bash, which is fine. Why don't we use the programming language we already use? Why duplicate a lot of stuff in a different language? I'd prefer this to be written in Rust. Pro side, no need to install any node interpreter or dependencies. Just a binary. And we already have a lot of CLI infrastructure.

@helio-frota
Copy link
Copy Markdown
Contributor

So if we're converting this away from Bash, which is fine.

✔️

gensbom was converted to TypeScript from Bash using Claude Code as the first step, then it was manually refactored and split into several files to reflect a modern TypeScript project structure.

Sounds good then this can be more easy to transform to Rust now 👍

Pro side, no need to install any node interpreter or dependencies

👍

. And we already have a lot of CLI infrastructure.

There are some other hidden issues in this section... one of them is taking care of npm dependencies (dependabot sending PRs, security fixes, future linting problems with updates, maybe other folks have ruby or python or java background before Rust, and etc...)

plus: Multi-"runtime"/platform maintenance adds extra work + editor/IDE configs and etc

@i386x
Copy link
Copy Markdown
Contributor Author

i386x commented Apr 16, 2026

So if we're converting this away from Bash, which is fine. Why don't we use the programming language we already use? Why duplicate a lot of stuff in a different language? I'd prefer this to be written in Rust. Pro side, no need to install any node interpreter or dependencies. Just a binary. And we already have a lot of CLI infrastructure.

Rust was also on my list of options, and now when I see how much pain the conversion to Type Script costs me I must give you 👍 . The reason why I choose Type Script over Rust was mainly to reuse some bits from front end's e2e tests. However having this feature as a sub-command to Trustify CLI seems to be the best option to me.

@ctron
Copy link
Copy Markdown
Contributor

ctron commented Apr 20, 2026

So my suggestion would then be, let's close this one and to it in Rust.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants