Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 151 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,16 @@ This guide walks you through both installation and usage.
1. [Branch Option](#branch-option)
3. [Path Scan](#path-scan)
1. [Terraform Plan Scan](#terraform-plan-scan)
4. [Commit History Scan](#commit-history-scan)
4. [Binary Scan](#binary-scan)
1. [Supported Artifacts](#supported-artifacts)
2. [Binary Scan Options](#binary-scan-options)
3. [How Components Are Identified](#how-components-are-identified)
4. [Unidentified Components](#unidentified-components)
5. [Limitations](#binary-scan-limitations)
5. [Commit History Scan](#commit-history-scan)
1. [Commit Range Option (Diff Scanning)](#commit-range-option-diff-scanning)
5. [Pre-Commit Scan](#pre-commit-scan)
6. [Pre-Push Scan](#pre-push-scan)
6. [Pre-Commit Scan](#pre-commit-scan)
7. [Pre-Push Scan](#pre-push-scan)
2. [Scan Results](#scan-results)
1. [Show/Hide Secrets](#showhide-secrets)
2. [Soft Fail](#soft-fail)
Expand All @@ -67,6 +73,9 @@ This guide walks you through both installation and usage.
6. [Ignoring via a config file](#ignoring-via-a-config-file)
9. [Report command](#report-command)
1. [Generating SBOM Report](#generating-sbom-report)
1. [Repository](#repository)
2. [Local Project](#local-project)
3. [Built Artifact](#built-artifact)
10. [Import command](#import-command)
11. [Scan logs](#scan-logs)
12. [Syntax Help](#syntax-help)
Expand Down Expand Up @@ -983,6 +992,127 @@ If you just have a configuration file, you can generate a plan by doing the foll

`cycode scan -t iac path ~/PATH/TO/YOUR/{tfplan}.json`

### Binary Scan

A binary scan examines a built Java artifact instead of a source tree.
Point the CLI at a JAR, WAR, EAR or Spring Boot fat JAR and it identifies the open-source components packaged inside it, then scans them exactly as an SCA scan of the source would.

This closes the gap between what was scanned and what was shipped.
It is the only option when you have no source at all: a vendor-supplied JAR, a legacy EAR whose build job no longer exists, or a release gate that should check the actual deployable rather than the commit that supposedly produced it.

To scan an artifact, execute the following:

`cycode scan -t sca binary {{path}}`

For example:

```shell
# a single deployable
cycode scan -t sca binary ./dist/payments.war

# every Java archive under a directory
cycode scan -t sca binary ./dist

# recurse further into deeply nested archives
cycode scan -t sca --max-depth 5 binary ./dist/payments.ear
```

Everything an SCA scan normally supports still applies: `--severity-threshold`, `--soft-fail`, `cycode ignore` rules, `--export`, `--cycode-report` and the usual exit codes.

> [!IMPORTANT]
> The artifact never leaves your machine.
> The CLI opens it locally and uploads only the resulting component inventory, which is typically a few tens of kilobytes regardless of how large the artifact is.

You can also extract archives encountered during an ordinary path scan, using `--include-binaries`:

`cycode scan -t sca --include-binaries path ./target`

This is off by default.
Extracting a large tree of artifacts takes real time, and a path scan that silently got slower would be a worse surprise than an opt-in flag.

#### Supported Artifacts

| Artifact | Recognized layouts |
|---|---|
| JAR | `lib/*.jar`, embedded Maven metadata |
| WAR | `WEB-INF/lib/*.jar` |
| EAR | nested `*.war` and `*.jar` modules, `APP-INF/lib/*.jar` |
| Spring Boot fat JAR | `BOOT-INF/lib/*.jar` |

Nested archives are opened recursively, so a JAR inside a WAR inside an EAR is scanned.

#### Binary Scan Options

> [!NOTE]
> These options belong to the `scan` command, so they must appear **before** the `binary` subcommand:
> `cycode scan -t sca --max-depth 5 binary app.ear`

| Option | Default | Description |
|---|---|---|
| `--max-depth` | `3` | Nested-archive recursion limit. An EAR containing WARs containing JARs is depth 3. |
| `--offline` | off | Identify components from embedded metadata only. Acknowledges and silences the partial-results warning. |
| `--maven-central` | off | Look up archives that embedded metadata cannot identify on Maven Central by SHA-1. Sends only the digest, never the archive. Cannot be combined with `--offline`. |
| `--project-name` | inferred | Override the platform identity when the artifact is detached from its source repository. |
| `--keep-bom` | off | Write the generated component inventory beside each artifact, for inspection or audit. |
| `--include-binaries` | off | On `scan path` only. Extract any Java archives encountered during the walk. |

Platform identity is inferred from the Git remote when you run inside a repository, and falls back to the artifact filename otherwise.
`--monitor` is refused on a bare filename identity, because monitoring keyed on `app.jar` would merge unrelated projects into one and quietly corrupt the trend data.
Use `--project-name` or run from inside the repository the artifact was built from.

#### How Components Are Identified

Components are identified from metadata the build itself wrote into the archive:

| Source | Confidence | Notes |
|---|---|---|
| `META-INF/maven/.../pom.properties` | exact | Written by Maven. Authoritative group, artifact and version. |
| Maven Central digest lookup (`--maven-central`) | exact | The SHA-1 of the archive, matched against what Maven Central published. Opt-in, because it is the one step that sends anything about the artifact off the machine. |
| `META-INF/MANIFEST.MF` attributes | low | `Implementation-Title`, OSGi `Bundle-SymbolicName` and similar, with the group taken from `Implementation-Vendor-Id`. Used only when all three parts are shaped like Maven coordinates; a product name or a build banner is not one. |

Every finding reports which source identified its component and where inside the artifact that component sits, so a hit on a large EAR points at a specific nested JAR rather than the whole file.

> [!NOTE]
> Findings from a low-confidence match are printed and exported, but do **not** affect the exit code.
> A wrong coordinate produces a wrong vulnerability list, and a fabricated CVE breaking a release costs more trust than the extra coverage is worth.

#### Unidentified Components

Archives that carry no usable metadata are listed in their own section by path, digest and size, and appear under an `unidentified` key in `--output json` so CI can assert on coverage:

```
╭─ 🔎 Unidentified (1) ────────────────────────────────────────────╮
│ Path SHA-1 Size │
│ payments.war > WEB-INF/lib/internal-shim.jar 5fe08079… 142 B │
╰──────────────────────────────────────────────────────────────────╯

3 identified (1 low confidence) | 1 unidentified | 17 vulnerabilities
```

The coverage line counts manifest-only matches as identified but calls them out, and `--output json` reports the same number as `binary.low_confidence_components`.

We do not guess a component's identity from its filename.
`internal-shim.jar` is not evidence of anything, and an admitted gap is more useful than an invented coordinate.
Unidentified components do not set the exit code on their own.

#### Binary Scan Limitations

Read this before relying on a binary scan as your only check.

- **Relocated and shaded classes are not detected.**
When a build rewrites `com.google.common` into `com.acme.shaded.common` and merges it into the parent JAR, there is no separate JAR to identify and no metadata left to read.
Those components will not appear in the results at all.
Detecting them requires class-level fingerprinting, which this feature does not do.
- **Source scanning gives a truer picture.**
A source scan resolves the real dependency graph from your lockfiles.
A binary scan sees what is physically packaged, and infers relationships from archive nesting plus any embedded `pom.xml` files it finds.
Where you have source, scan the source; use binary scanning for the artifacts you cannot scan any other way.
- **Coverage is reported, not assumed.**
The coverage line is always printed and always true.
If it says components could not be identified, the scan is genuinely incomplete for those components rather than clean.
- **Java only, for now.**
.NET, npm and container artifacts are not supported yet.

### Commit History Scan

> [!NOTE]
Expand Down Expand Up @@ -1533,6 +1663,7 @@ The following commands are available for use with this command:
|------------------|-----------------------------------------------------------------|
| `path` | Generate SBOM report for provided path in the command |
| `repository-url` | Generate SBOM report for provided repository URI in the command |
| `binary` | Generate SBOM report for a built Java artifact (JAR, WAR, EAR) |

### Repository

Expand All @@ -1558,6 +1689,23 @@ The `path` subcommand supports the following additional options:
| `--gradle-all-sub-projects` | Run the Gradle restore command for all sub-projects (use from the root of a multi-project Gradle build). |
| `--maven-settings-file` | For Maven only, allows using a custom [settings.xml](https://maven.apache.org/settings.html) file when building the dependency tree. |

### Built Artifact

To create an SBOM report for a built Java artifact, without scanning it for vulnerabilities:\
`cycode report sbom --format <sbom format> --output-file </path/to/file> binary </path/to/artifact>`

For example:\
`cycode report sbom --format spdx-2.3 --output-file payments-sbom.json binary ./dist/payments.war`

This answers the compliance case directly: an SBOM of what you actually shipped, rather than of what was committed.
It uses the same extraction as [Binary Scan](#binary-scan), so the [limitations](#binary-scan-limitations) documented there apply here too — in particular, shaded and relocated components will be missing from the SBOM.

The `binary` subcommand supports the following additional option:

| Option | Description |
|---------------|----------------------------------------------------------------------------------------------|
| `--max-depth` | Nested-archive recursion limit. Defaults to 3. An EAR containing WARs containing JARs is depth 3. |

# Import Command

## Importing SBOM
Expand Down
2 changes: 2 additions & 0 deletions cycode/cli/apps/report/sbom/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import typer

from cycode.cli.apps.report.sbom.binary.binary_command import binary_command
from cycode.cli.apps.report.sbom.path.path_command import path_command
from cycode.cli.apps.report.sbom.repository_url.repository_url_command import repository_url_command
from cycode.cli.apps.report.sbom.sbom_command import sbom_command
Expand All @@ -10,6 +11,7 @@
app.command(name='repository-url', short_help='Generate SBOM report for provided repository URI in the command.')(
repository_url_command
)
app.command(name='binary', short_help='Generate SBOM report for a built Java artifact (JAR, WAR, EAR).')(binary_command)

# backward compatibility
app.command(hidden=True, name='repository_url')(repository_url_command)
Empty file.
115 changes: 115 additions & 0 deletions cycode/cli/apps/report/sbom/binary/binary_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import time
from pathlib import Path
from typing import Annotated

import typer

from cycode.cli import consts
from cycode.cli.apps.report.sbom.common import create_sbom_report, send_report_feedback
from cycode.cli.exceptions.handle_report_sbom_errors import handle_report_exception
from cycode.cli.files_collector.binary.collector import collect_binary_documents
from cycode.cli.files_collector.zip_documents import zip_documents
from cycode.cli.utils.get_api_client import get_report_cycode_client
from cycode.cli.utils.progress_bar import SbomReportProgressBarSection

_REPORT_COMMAND_TYPE = 'binary'


def binary_command(
ctx: typer.Context,
path: Annotated[
Path,
typer.Argument(
exists=True,
resolve_path=True,
help='Path to the built artifact to generate an SBOM for.',
show_default=False,
),
],
max_depth: Annotated[
int,
typer.Option('--max-depth', help='Nested-archive recursion limit.', min=1),
] = consts.BINARY_MAX_DEPTH,
maven_central: Annotated[
bool,
typer.Option(
'--maven-central',
help='Look archives that embedded metadata cannot identify up on Maven Central by SHA-1. '
'Sends the digest of each such archive, never the archive itself, to search.maven.org.',
),
] = False,
) -> None:
""":package: [bold cyan]Generate an SBOM for a built Java artifact.[/]

Reads a JAR, WAR, EAR or Spring Boot fat JAR and produces an SBOM of the open-source components inside it,
without scanning them for vulnerabilities. Answers the compliance case directly: an SBOM of what shipped,
rather than of what was committed.

Example usage:
* `cycode report sbom --format cyclonedx-1.4-json binary app.war`
* `cycode report sbom --format spdx-2.3-json binary app.ear`

Format conversion happens server-side, so every format the path command supports is supported here too.

"""
ctx.obj['binary_max_depth'] = max_depth
ctx.obj['maven_central'] = maven_central

client = get_report_cycode_client(ctx)
report_parameters = ctx.obj['report_parameters']
output_format = report_parameters.output_format
output_file = ctx.obj['output_file']

progress_bar = ctx.obj['progress_bar']
progress_bar.start()

start_scan_time = time.time()
report_execution_id = -1

try:
# the only difference from the path command: our collector in place of the manifest walk. Everything from
# zip_documents onward is reused verbatim, and the server generates the document.
collection = collect_binary_documents(
ctx,
(str(path),),
stop_on_error=ctx.obj.get('stop_on_error', False),
progress_bar_section=SbomReportProgressBarSection.PREPARE_LOCAL_FILES,
)
ctx.obj['binary_result'] = collection

if not collection.documents:
raise typer.BadParameter(
f'No supported binary artifacts were found at {str(path)!r}. '
'Supported artifacts are .jar, .war and .ear files.',
param_hint='PATH',
)

zipped_documents = zip_documents(consts.SCA_SCAN_TYPE, collection.documents)
report_execution = client.request_sbom_report_execution(report_parameters, zip_file=zipped_documents)
report_execution_id = report_execution.id

create_sbom_report(progress_bar, client, report_execution_id, output_file, output_format)

send_report_feedback(
client=client,
start_scan_time=start_scan_time,
report_type='SBOM',
report_command_type=_REPORT_COMMAND_TYPE,
request_report_parameters=report_parameters.to_dict(without_entity_type=False),
report_execution_id=report_execution_id,
request_zip_file_size=zipped_documents.size,
)
except Exception as e:
progress_bar.stop()

send_report_feedback(
client=client,
start_scan_time=start_scan_time,
report_type='SBOM',
report_command_type=_REPORT_COMMAND_TYPE,
request_report_parameters=report_parameters.to_dict(without_entity_type=False),
report_execution_id=report_execution_id,
error_message=str(e),
)

handle_report_exception(ctx, e)
2 changes: 2 additions & 0 deletions cycode/cli/apps/scan/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import typer

from cycode.cli.apps.scan.binary.binary_command import binary_command
from cycode.cli.apps.scan.commit_history.commit_history_command import commit_history_command
from cycode.cli.apps.scan.path.path_command import path_command
from cycode.cli.apps.scan.pre_commit.pre_commit_command import pre_commit_command
Expand All @@ -23,6 +24,7 @@

app.command(name='path', short_help='Scan the files in the paths provided in the command.')(path_command)
app.command(name='repository', short_help='Scan the Git repository included files.')(repository_command)
app.command(name='binary', short_help='Scan built Java artifacts (JAR, WAR, EAR, Spring Boot).')(binary_command)
app.command(name='commit-history', short_help='Scan commit history or perform diff scanning between specific commits.')(
commit_history_command
)
Expand Down
Empty file.
58 changes: 58 additions & 0 deletions cycode/cli/apps/scan/binary/binary_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from pathlib import Path
from typing import Annotated

import typer

from cycode.cli.apps.scan.binary.identity import (
assert_monitor_has_an_explicit_identity,
resolve_platform_identity,
)
from cycode.cli.apps.scan.code_scanner import scan_binary_artifacts
from cycode.cli.logger import logger


def binary_command(
ctx: typer.Context,
paths: Annotated[
list[Path],
typer.Argument(
exists=True,
resolve_path=True,
help='Paths to the built artifacts to scan',
show_default=False,
),
],
) -> None:
""":package: [bold cyan]Scan built Java artifacts for open-source vulnerabilities.[/]

Opens a JAR, WAR, EAR or Spring Boot fat JAR, identifies the open-source components inside it, and scans them
exactly as a source scan would. The artifact never leaves your machine: only the component inventory is
uploaded.

Example usage:
* `cycode scan -t sca binary app.war`: Scan a single deployable.
* `cycode scan -t sca binary dist/`: Scan every Java archive under a directory.
* `cycode scan -t sca --max-depth 5 binary app.ear`: Recurse further into nested archives.

Components are identified from embedded Maven metadata. Anything that cannot be identified is reported in its
own section rather than guessed at. Relocated and shaded classes are not detected: where source is available,
a source scan gives a truer dependency graph.

"""
tuple_paths = tuple(str(path) for path in paths)

identity = resolve_platform_identity(ctx, tuple_paths)
if ctx.obj.get('monitor'):
assert_monitor_has_an_explicit_identity(identity)

ctx.obj['binary_identity'] = identity

progress_bar = ctx.obj['progress_bar']
progress_bar.start()

logger.debug(
'Starting binary scan process, %s',
{'paths': paths, 'identity': identity.value, 'identity_source': identity.source},
)

scan_binary_artifacts(ctx, tuple_paths)
Loading