Skip to content

PM-4777: Binary scanning for Java artifacts, with declared and transitive dependency reporting - #539

Open
dineshmistry wants to merge 2 commits into
cycodehq:mainfrom
dineshmistry:PM-4777-binary-composition-analysis
Open

PM-4777: Binary scanning for Java artifacts, with declared and transitive dependency reporting#539
dineshmistry wants to merge 2 commits into
cycodehq:mainfrom
dineshmistry:PM-4777-binary-composition-analysis

Conversation

@dineshmistry

Copy link
Copy Markdown

Supersedes #524, which was closed on 1 September before the second commit landed. Same branch, two commits.

What

Adds binary composition analysis to the CLI: point it at a built Java artifact and get the same SCA findings a source scan gives, plus an explicit account of what could not be identified.

cycode scan -t sca binary app.war
cycode scan -t sca binary dist/
cycode report sbom --format cyclonedx-1.4-json binary app.ear

Supported: JAR, WAR, EAR, Spring Boot fat JAR, nested archives to --max-depth (default 3).

How it works

  1. A hardened zip reader walks the archive in memory. Only nested archives and Maven metadata entries are read; class files and resources are never touched, and archive bytes are never uploaded or written to disk.
  2. Each library is identified by a ladder: META-INF/maven/**/pom.properties (exact) → optional Maven Central SHA-1 lookup with --maven-central (exact, opt-in, sends the hash only) → MANIFEST.MF attributes when they are shaped like real coordinates and carry a declared group (marked ambiguous, never gates the exit code) → otherwise unidentified, reported by path, digest and size.
  3. A CycloneDX 1.4 document is synthesised with one component per identified coordinate, cycode:evidence / cycode:confidence / cycode:path properties, and a dependency graph from containment overlaid with real edges from embedded pom.xml.
  4. That document, plus a fixed synthetic pom.xml (the engine routes a bare bom.json to nothing), goes through the existing zip_documentsapi/v4/scans/cli path. Nothing below scan_documents changes.

Product stance

  • No guessing. A coordinate comes from metadata the build wrote or from an exact hash match. A filename or a manifest banner never becomes a component. A wrong coordinate produces a wrong CVE list.
  • Unidentified is a first-class result. Listed in the console and under binary.unidentified in JSON, with a coverage line that is always printed and always true: 9 identified (2 low confidence) | 7 unidentified | 23 vulnerabilities.
  • Declared is not shipped. Embedded poms draw edges between components that were actually found; they do not add components. Source scanning remains the tool for the full dependency graph, and the README says so.

Options

Flag Default
--max-depth 3 Nested-archive recursion limit
--maven-central off Resolve unidentified archives by SHA-1 on search.maven.org (hash only). Refused with --offline
--offline off Embedded metadata only; acknowledges and silences the partial-coverage warning
--project-name inferred Platform identity when detached from a Git checkout; required for --monitor on a bare filename
--keep-bom off Write the synthesised document beside each artifact
--include-binaries off On scan path, also extract Java archives met during the walk

Security hardening in the reader

Zip-slip and absolute/drive/UNC paths, symlinks and non-regular entries, per-entry and total size limits, compression-ratio and entry-count bombs, truncated central directories, and duplicate entry names (Python's ZipFile.open(name) returns the last duplicate; every entry is read from its own ZipInfo so a vulnerable jar cannot hide behind a patched one with the same name). Embedded pom.xml is refused if it carries a DOCTYPE or ENTITY declaration, which closes entity expansion without adding defusedxml. Console output from entry names is stripped of rich markup, control characters and unbounded length.

Verification

  • 1,364 tests pass; ~94% coverage on the new modules; ruff check and ruff format --check clean; Python 3.9 floor respected; all HTTP in tests mocked with responses, including the Maven Central client.
  • Positive control before every corpus run: a WAR built from real Maven Central jars yields 23 findings including CVE-2021-44228.
  • Real vendor corpus (16 jars including SAP- and Oracle-licensed builds) scanned with and without lookup and cross-checked against Checkmarx, Trivy and Syft/Grype. On files physically present all four tools agree.

Declared and transitive dependencies (second commit)

A customer's Checkmarx report on 16 vendor jars listed 29 vulnerabilities, every one on a dependency the jars declare in their embedded pom but do not ship. Three opt-in flags on scan binary and report sbom binary close that gap:

Flag What it adds
--include-declared Reads every embedded pom.xml the way Maven does (properties, parent chain, dependencyManagement incl. imported BOMs, inherited parent dependencies) and reports compile/runtime dependencies that were not shipped as components marked cycode:presence=declared, no hashes, path = the declaring pom. Test/provided/system/optional are left out: Maven never hands them to a consumer.
--include-test-scope Lifts that filter so totals line up with tools that count every declared dependency. Scope stays on each component.
--include-transitive Follows each declared dependency through its own pom on Maven Central: nearest wins, exclusions (incl. wildcards) down the branch, optional stops, Maven's scope table, root dependencyManagement overrides. Records cycode:via and links the graph. Bounded to 10 hops and 250 poms per embedded pom, cached per run. Requires --maven-central.

Parent and dependency poms come from repo1.maven.org behind a PomSource seam. Coordinates read from an untrusted pom are validated before they can shape a URL; bodies are capped at 1 MB; a version that cannot be established is listed under "Declared, version unresolved" with the reason, never guessed. The backend was probed first: it evaluates a bare pom only for literal and in-file-property versions, drops test scope, and resolves neither parents nor transitives, so the resolution has to live in the CLI.

Result on the same 16 jars with all three flags: all 61 declared packages on the Checkmarx list are reported, 30 findings to Checkmarx's 29, the difference being vulnerability-database content only. Run time about 40 seconds. 1,364 tests pass.

Not in this PR

  • Cycode-side digest index (Tier 2 proper): behind the DigestResolver seam; needs a backend endpoint that does not exist yet.
  • Shaded/relocated classes: no metadata survives; documented as a limitation.
  • .NET, npm, containers: the BinaryExtractor interface is ready; Java only ships now.
  • CycloneDX schema validation: would need jsonschema; a structural validator ships instead.
  • Pre-existing on main, not touched: report sbom prints "Report saved to" and writes no file (reproduces on report sbom path .).

Note on the ticket prefix

Branch and title use the product ticket PM-4777. If a CM- ticket should own this, the title and branch are a rename away.

🤖 Generated with Claude Code

https://claude.ai/code/session_016SxZLL1buHZ8jZ9T8onEZ8

…g Boot)

Adds `cycode scan -t sca binary <path>` and `cycode report sbom binary <path>`.
The CLI opens a Java archive locally, identifies the open-source components
inside it from embedded Maven metadata (pom.properties, then optionally a
Maven Central SHA-1 lookup, then MANIFEST.MF), synthesises a CycloneDX 1.4
document, and feeds it to the existing SCA scan path. The archive itself is
never uploaded.

What ships:
- Hardened zip reader (zip-slip, symlinks, absolute paths, compression and
  entry-count bombs, duplicate entry names read by central-directory record).
- Identification ladder with an explicit `unidentified` result; no coordinate
  is ever guessed from a filename or an unshaped manifest value.
- Dependency graph from containment plus real edges from embedded pom.xml
  (parsed with a DOCTYPE/ENTITY guard, no defusedxml needed).
- `--max-depth`, `--offline`, `--maven-central` (opt-in, sends the hash only),
  `--project-name`, `--keep-bom`, `--include-binaries`.
- Coverage line and JSON fields so CI can gate on identification, not guess.
- `DigestResolver` seam so the Cycode backend digest index can replace the
  Maven Central implementation without caller changes.

No new runtime dependency. All HTTP in tests is mocked with `responses`.
Adds three opt-in flags to `scan binary` and `report sbom binary`:

- `--include-declared` reads every embedded pom.xml the way Maven does
  (properties, ${project.*}, the parent chain, dependencyManagement with
  imported BOMs, dependencies inherited from parents) and reports the
  compile- and runtime-scope dependencies the artifact does not ship as
  components marked `declared`, with no hashes and the declaring pom as
  their path. Test, provided, system and optional are left out because
  Maven never hands them to a consumer.
- `--include-test-scope` lifts that filter so totals line up with SCA
  tools that count every declared dependency. Scope stays on each
  component.
- `--include-transitive` follows each declared dependency through its
  own pom on Maven Central: nearest declaration wins, exclusions apply
  down their branch, optional stops, scope combines per Maven's table,
  and the root pom's management overrides transitive versions. Each
  transitive records what pulled it in (`cycode:via`) and the graph
  links them. Bounded to 10 hops and 250 poms per embedded pom.

Parent and dependency poms come from repo1.maven.org behind a PomSource
seam, so `--maven-central` is required for parent-managed versions and
for transitives. Coordinates read from an untrusted pom are validated
before they can shape a URL, and bodies are capped at 1 MB. A version
that cannot be established is listed under "Declared, version
unresolved" with the reason, never guessed.

Motivation: a customer's Checkmarx report on 16 vendor jars listed 29
vulnerabilities, every one on a declared-but-unshipped dependency. With
all three flags Cycode reports all 61 of those declared packages and 30
findings; the difference is vulnerability-database content only.

Claude-Session: https://claude.ai/code/session_016SxZLL1buHZ8jZ9T8onEZ8

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

if not chunk:
break

digest.update(chunk)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cycode: SAST violation: 'Usage of weak hashing library (SHA-1)'.

Risk Score: 65 (MEDIUM)
Severity: Medium

Description

Using a weak hashing library like SHA-1 increases the risk of data breaches. SHA-1 in particular is vulnerable to collision attacks, where two different inputs can produce the same hash value, compromising data integrity and security.

Cycode Remediation Guideline

✅ Do


  • Do opt for stronger hashing algorithms such as SHA-256 to enhance security.
    hashlib.sha256('password').digest()

❌ Don't


  • Do not use SHA-1 for hashing. It is no longer considered secure due to its vulnerability to collision attacks.
    hashlib.sha1('password').digest() # unsafe

🎥 Learning materials (by Secure Code Warrior)


Tell us how you wish to proceed using one of the following commands:

Tag Short Description
#cycode_sast_false_positive <reason> Mark as false positive — applies to this violation only
#cycode_sast_ignore_here <reason> Ignore this violation — applies to this violation only
#cycode_ai_remediation Request remediation guidance using Cycode AI

⚠️ When commenting on Github, you may need to refresh the page to see the latest updates.

self._check_compression_ratio(entry, read_bytes)
self._budget.consume(len(chunk))

sha1.update(chunk)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cycode: SAST violation: 'Usage of weak hashing library (SHA-1)'.

Risk Score: 65 (MEDIUM)
Severity: Medium

Description

Using a weak hashing library like SHA-1 increases the risk of data breaches. SHA-1 in particular is vulnerable to collision attacks, where two different inputs can produce the same hash value, compromising data integrity and security.

Cycode Remediation Guideline

✅ Do


  • Do opt for stronger hashing algorithms such as SHA-256 to enhance security.
    hashlib.sha256('password').digest()

❌ Don't


  • Do not use SHA-1 for hashing. It is no longer considered secure due to its vulnerability to collision attacks.
    hashlib.sha1('password').digest() # unsafe

🎥 Learning materials (by Secure Code Warrior)


Tell us how you wish to proceed using one of the following commands:

Tag Short Description
#cycode_sast_false_positive <reason> Mark as false positive — applies to this violation only
#cycode_sast_ignore_here <reason> Ignore this violation — applies to this violation only
#cycode_ai_remediation Request remediation guidance using Cycode AI

⚠️ When commenting on Github, you may need to refresh the page to see the latest updates.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant