Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/findings-container-sca.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@codacy/codacy-cloud-cli": minor
---

`findings` can now narrow to container scanning: `--scan-types` accepts `ContainerSCA` (aliases `ContainerScanning` and `Container`), and the new `--image <name>` (or `name:tag`) and `--image-tag <tag>` options filter findings to one container image. Container findings show their image as `name:tag` in place of the repository, on both `findings` cards and the `finding` detail view, and include `imageName`/`imageTag` in `--output json`.
1 change: 1 addition & 0 deletions SPECS/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ This is the single source of truth for all project tasks and specs.

| Date | What was done |
|---|---|
| 2026-11-09 | (OD-1049) `findings` can narrow to container scanning. **`--scan-types` accepts `ContainerSCA`**, the API value (as the `SearchSRMItems.scanTypes` example in the spec lists it), not `ContainerScanning` as the ticket named it; `ContainerScanning` and `Container` are accepted as aliases, normalized like the other scan types. The normalized list is now de-duplicated, since several aliases resolve to one value. **`-i, --image <name>` and `-t, --image-tag <tag>`** map to the body's `containerImage` (`ContainerImageFilter`), through `parseImageFilter`: values are trimmed, `--image name:tag` is split (a registry port stays in the name), and empty values, a tag without a name, a digest reference or two different tags are refused before any request. An empty `--image` would otherwise be dropped by a truthiness check and silently widen the result to the whole org. **Container findings show `image:tag` in place of the repository** on the `findings` card (also in the single-repository view) and on the `finding` header, through the shared `formatImageReference`; both commands' JSON gains `imageName`/`imageTag` (25 new tests, 823 total) |
| 2026-11-03 | **Node.js engines raised for CVE-2026-111497** (Brotli decoder OOB write, bundled in Node). `engines` `>=20` → `^22.23.4 \|\| ^24.22.1 \|\| >=26.12.1`, the first patched release of each line; Node 20 is EOL and unpatched, so it is dropped. CI matrix `[20, 22]` → `[22, 24]`. No dependency change: the repo ships no Brotli package, but `fetch` (undici) decompresses `Content-Encoding: br` responses with Node's bundled decoder, so the runtime is the exposure. `engines` only warns on install; no runtime version check was added |
| 2026-09-30 | (OD-590, OD-784) `fixedVersion` surfaced on `issue`/`issues`/`pull-request --issue`, now that `CommitIssue` carries it (first in API `57.7.9`; counterpart of `codacy-spa` #3145). **API bump: pinned `57.6.4` → `57.7.10`** — a diff of both bundled specs shows only `CommitIssue.fixedVersion` added, the organization webhook operations retagged `organization` → `webhooks` (the CLI uses none of them) and a `hasWebhooksEnabled` field on an organization schema; no operation signatures shifted, so `SPECS/repository-tokens.md`'s whitelist needed no changes. `57.7.9` and `57.7.10` bundle identical specs. `printIssueCard` (list) and `printIssueCodeContext` (detail, shared with `pull-request --issue` and `finding`'s linked-issue block) now pass `issue.fixedVersion` to the existing `formatDependencyChainsLine`/`formatDependencyChainsBlock`, so a direct dependency reads `Direct - Update <pkg> to <version>` and a transitive one ends `(Fixed in <version>)`, as on `finding`/`findings`. The param `finding` passes (`SrmItem.fixedVersion`) still wins when non-empty. An empty array (no fix available) renders as before — no version text — rather than adding a "no fix" phrase the other commands don't use. `fixedVersion` added to all three JSON projections (issue.test.ts 24→28, issues.test.ts 76→78, pull-request.test.ts +2) |
| 2026-09-23 | (OD-748) `images` shows a **Tags** column (per-image `tagCount`) and an `Image tags: X of Y used` line under the header, read from `listOrganizationImages`' new `usage` object (OD-724, API 57.6.4 — `fetch-api` bumped from 57.4.17). Still one request per page. The line turns red at the cap, where new tags are rejected. Exact figures, not `formatCount`. `--output json` stays an array and gains `tagCount` only; `usage` is not in the JSON, since adding it would change the top-level shape The `image --delete --keep-latest` budget warning now reads that same `usage.limit` from the image-count request it already made, in place of the hardcoded `DEFAULT_ORG_TAG_CAP` (1,000), and drops its "this CLI cannot read the value in force" disclaimer Both commands tolerate a response without `usage`/`tagCount` (an API behind the client), and the exact-figure formatter is now shared as `formatExactCount` (5 new tests, 784 total) |
Expand Down
8 changes: 6 additions & 2 deletions SPECS/commands/finding.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The `findingId` is the UUID shown in dim gray at the end of each findings card.
## Output Format

```
{Priority colored} | {SecurityCategory} {ScanType} | {Optional: Likelihood} {EffortToFix} | {Optional: Repository} {id dimmed}
{Priority colored} | {SecurityCategory} {ScanType} | {Optional: Likelihood} {EffortToFix} | {Optional: Image or Repository} {id dimmed}
{Finding title}

{Status} {DueAt} | {Optional: CVE/CWE} | {Optional: AffectedVersion → FixedVersion} | {Optional: Application} | {Optional: AffectedTargets}
Expand Down Expand Up @@ -77,6 +77,10 @@ For Codacy-source findings, the CVE block is injected between the code context a

When `item.advisoryInformation` is present, shows the shared `printAdvisoryBlock` (advisory ID header, optional published date, one bullet per vulnerable function) via `utils/formatting.ts` — the same renderer used by `issue`/`pull-request --issue`. Shown here only when there is **no** linked Codacy issue; when there is one, `printIssueCodeContext` already renders the equivalent block from `issue.advisoryInformation`, so this avoids a duplicate. This is what makes vulnerable functions visible for SCA/dependency findings and any other non-Codacy-source finding, which have no linked issue to borrow the block from. Included in `--output json` as `finding.advisoryInformation.{advisoryId,vulnerableFunctions,publishedAt}`.

### Container image (container scanning)

When `item.imageName` is present, the header line shows the scanned image as `name:tag` (just `name` without `imageTag`) **in place of the repository**, matching the `findings` card the id was picked from. Built by the shared `formatImageReference` (`utils/formatting.ts`) and sanitized. Included in `--output json` as `finding.imageName` / `finding.imageTag`.

## Dependency import chains (SCA)

When a finding carries `dependencyChains` (`string[][]`), **all** chains are listed
Expand All @@ -97,4 +101,4 @@ carries the fixed version — `CommitIssue` itself has no `fixedVersion` field t

## Tests

File: `src/commands/finding.test.ts` — 29 tests (27 + 2 for the linked-issue dependency-chain dedup).
File: `src/commands/finding.test.ts` — 31 tests (27 + 2 for the linked-issue dependency-chain dedup + 2 for the container image).
23 changes: 20 additions & 3 deletions SPECS/commands/findings.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,32 @@ codacy find gh my-org my-repo --output json
| `--severities <list>` | `-s` | Comma-separated priority levels: Critical, High, Medium, Low |
| `--statuses <list>` | `-S` | Comma-separated statuses: Overdue, OnTrack, DueSoon, ClosedOnTime, ClosedLate, Ignored |
| `--categories <list>` | `-c` | Comma-separated security category names |
| `--scan-types <list>` | `-T` | Comma-separated scan types: SAST, Secrets, SCA, CICD, IaC, DAST, PenTesting, License, CSPM |
| `--scan-types <list>` | `-T` | Comma-separated scan types: SAST, Secrets, SCA, ContainerSCA (aliases: ContainerScanning, Container), CICD, IaC, DAST, PenTesting, License, CSPM. Case-insensitive; spaces, hyphens and underscores are ignored |
| `--dast-targets <list>` | `-d` | Comma-separated DAST target URLs |
| `--image <name>` | `-i` | Container image name, or `name:tag` (sent as `containerImage.name` / `.tag`) |
| `--image-tag <tag>` | `-t` | Container image tag (sent as `containerImage.tag`). Requires `--image`: the API rejects a tag without a name |

Default status filter: `Overdue,OnTrack,DueSoon`.

`--image` / `--image-tag` are validated by `parseImageFilter`, before git auto-detection or any request, and refused rather than sent when the API would mishandle them silently:

- **Empty or whitespace-only values.** A truthiness check would drop `--image ""`, widening the result to every finding in the org (a script's `--image "$IMG"` with `IMG` unset). Both values are trimmed.
- **`--image-tag` without an image name.**
- **A digest reference** (`name@sha256:…`): the API filters by tag only.
- **Two different tags**, one in `--image name:tag` and one in `--image-tag`. The same tag given both ways is accepted.

`--image name:tag` is split into name and tag, because the cards print the image in that form and copying it back is the natural next step. Only a colon after the last `/` starts a tag, so a registry port (`registry:5000/app`) stays part of the name.

They map to the request body's single `ContainerImageFilter` (one image per request, not a list — hence singular names). `--image-tag` rather than `--tag` so it reads as narrowing the image, not as a finding tag. `--image` does not add `ContainerSCA` to `--scan-types` implicitly; the API applies the filter itself.

## Output

Card-style format:

```
────────────────────────────────────────

{Priority colored} | {SecurityCategory} {ScanType} | {Optional: Likelihood} {Optional: EffortToFix} | {Optional: Repository} {id dimmed}
{Priority colored} | {SecurityCategory} {ScanType} | {Optional: Likelihood} {Optional: EffortToFix} | {Optional: Image or Repository} {id dimmed}
{Finding title}
{Optional: affectedTargets}

Expand Down Expand Up @@ -73,6 +86,10 @@ is dropped from the status line (it would duplicate the chain line).

When an item carries `advisoryInformation` (`{advisoryId, vulnerableFunctions, publishedAt}`), a compact `Vulnerable functions: fn1, fn2 (+N more)` line is shown below the status/chain lines, via the shared `summarizeFunctions` helper (`utils/formatting.ts`, capped at 3 entries) — same rendering as `issue`/`pull-request`'s card view. Included in `--output json` as `advisoryInformation.{advisoryId,vulnerableFunctions,publishedAt}` per item.

### Container image (container scanning)

When an item carries `imageName` (container scanning findings), the header line shows the scanned image as `name:tag` (just `name` when `imageTag` is absent), dimmed and sanitized, **in place of the repository**: the finding is about an image, not a repository. Unlike the repository, the image is shown in the single-repository view too, since the command's arguments don't imply it. The switch is on `imageName` being present, not on `scanType === "ContainerSCA"`. `--output json` includes `imageName` and `imageTag` per item.

## Tests

File: `src/commands/findings.test.ts` — 27 tests (24 + 3 for advisoryInformation).
File: `src/commands/findings.test.ts` — 48 tests (incl. 9 unit tests for `parseImageFilter`).
4 changes: 4 additions & 0 deletions src/commands/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,10 @@ Dependency-chain helpers shared between `findings.ts` (list) and `finding.ts` (d
- `formatDependencyChainsLine(chains, fixedVersion)` — one-line list summary: first chain with its Direct/Transitive label + `... and N more`; returns `null` for no chains
- `formatDependencyChainsBlock(chains, fixedVersion)` — multi-line detail block: all chains, label shown once, continuation lines aligned under the label; returns `null` for no chains

Container-image helper shared between `findings.ts` (list) and `finding.ts` (detail):
- `formatImageReference(imageName, imageTag)` — `name:tag` (or `name`) for a container scanning finding, `undefined` without an image. Both commands show it **in place of the repository** on the header line (`findings` also in its single-repository view, where the repository is hidden). Returns the raw value; callers sanitize before styling
- `parseImageFilter(image, imageTag)` (exported from `findings.ts`, not shared) builds the `containerImage` request filter and **refuses** input the API would mishandle silently — empty values (a truthiness check would drop `--image ""` and widen to the whole org), a tag without a name, a digest, conflicting tags — and splits `--image name:tag`, treating only a colon after the last `/` as a tag separator so a registry port stays in the name

Advisory-information helpers shared between `issue.ts`/`pull-request.ts` (via `CommitIssue.advisoryInformation`) and `finding.ts`/`findings.ts` (via `SrmItem.advisoryInformation`) — same `AdvisoryInformation` shape from either source:
- `summarizeFunctions(fns, limit = 3)` — compact `fn1, fn2 (+N more)` string for card views (`issue`/`pull-request` cards, `findings` list)
- `printAdvisoryBlock(advisory)` — full detail block: bold `Vulnerable Functions (<advisoryId>)` header, optional `Published:` date, one bullet per function; used by `printIssueCodeContext` (issue detail views) and directly by `finding.ts` when there's no linked Codacy issue to render it via that path
Expand Down
42 changes: 42 additions & 0 deletions src/commands/finding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,48 @@ describe("finding command", () => {
expect(output).not.toContain("0.1.2 → 0.1.5");
});

describe("container image", () => {
const containerFinding = {
...mockScaFinding,
id: "img-123",
scanType: "ContainerSCA",
imageName: "codacy/codacy-website",
imageTag: "1.25.3",
};

it("should show image:tag instead of the repository on the header line", async () => {
vi.mocked(SecurityService.getSecurityItem).mockResolvedValue({
data: containerFinding,
} as any);

const program = createProgram();
await program.parseAsync(["node", "test", "finding", "gh", "test-org", "img-123"]);

// The header line is the only line that carries the finding id.
const header = vi
.mocked(console.log)
.mock.calls.map((c) => String(c[0] ?? ""))
.find((l) => l.includes("img-123"));
expect(header).toContain("codacy/codacy-website:1.25.3");
expect(header).not.toContain("my-repo");
});

it("should include imageName and imageTag in JSON output", async () => {
vi.mocked(SecurityService.getSecurityItem).mockResolvedValue({
data: containerFinding,
} as any);

const program = createProgram();
await program.parseAsync([
"node", "test", "--output", "json", "finding", "gh", "test-org", "img-123",
]);

const json = JSON.parse(vi.mocked(console.log).mock.calls[0][0]);
expect(json.finding.imageName).toBe("codacy/codacy-website");
expect(json.finding.imageTag).toBe("1.25.3");
});
});

it("should include dependencyChains in JSON output", async () => {
vi.mocked(SecurityService.getSecurityItem).mockResolvedValue({
data: mockScaWithChains,
Expand Down
11 changes: 9 additions & 2 deletions src/commands/finding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
formatDueDate,
formatVersionSegment,
formatDependencyChainsBlock,
formatImageReference,
printCveBlock,
printAdvisoryBlock,
printIssueCodeContext,
Expand Down Expand Up @@ -47,7 +48,7 @@ function printFindingDetail(

console.log();

// Line 1: Priority | SecurityCategory ScanType | Likelihood EffortToFix | Repository <id>
// Line 1: Priority | SecurityCategory ScanType | Likelihood EffortToFix | Repository or Image <id>
const line1Parts: string[] = [colorPriority(item.priority)];

const catParts = [
Expand All @@ -63,7 +64,11 @@ function printFindingDetail(
) as string[];
if (penTestParts.length > 0) line1Parts.push(penTestParts.join(" "));

if (item.repository) line1Parts.push(ansis.dim(sanitizeText(item.repository)));
// A container scanning finding shows its image:tag in the repository's
// slot, the same as on the `findings` card it was picked from.
const image = formatImageReference(item.imageName, item.imageTag);
if (image) line1Parts.push(ansis.dim(sanitizeText(image)));
else if (item.repository) line1Parts.push(ansis.dim(sanitizeText(item.repository)));

const idLabel = ansis.hex("#555555")(item.id);
console.log(line1Parts.join(pipe) + ` ${idLabel}`);
Expand Down Expand Up @@ -276,6 +281,8 @@ Examples:
"finding.fixedVersion",
"finding.application",
"finding.affectedTargets",
"finding.imageName",
"finding.imageTag",
"finding.dependencyChains",
"finding.ignored",
"finding.summary",
Expand Down
Loading
Loading