diff --git a/.github/scripts/sync-skills-vendor.json b/.github/scripts/sync-skills-vendor.json new file mode 100644 index 0000000..b4c4d06 --- /dev/null +++ b/.github/scripts/sync-skills-vendor.json @@ -0,0 +1,5 @@ +{ + "repo": "jfrog/jfrog-skills", + "pin": "v0.11.0", + "paths": ["skills"] +} diff --git a/.github/scripts/sync-skills.mjs b/.github/scripts/sync-skills.mjs new file mode 100644 index 0000000..2d42520 --- /dev/null +++ b/.github/scripts/sync-skills.mjs @@ -0,0 +1,117 @@ +#!/usr/bin/env node +// Vendors skill content from the upstream jfrog/jfrog-skills repository +// into this plugin. Run manually when bumping the pin: bump `pin` in +// sync-skills-vendor.json, then run this +// script to regenerate `skills/`, then commit both alongside each other. +// +// Usage: +// node .github/scripts/sync-skills.mjs +// +// Steps the script performs: +// 1. Reads sync-skills-vendor.json to learn which repo + ref to pull. +// 2. Downloads that tarball from codeload.github.com (public, no auth). +// 3. Extracts it into a temp directory. +// 4. Copies the requested paths (e.g. "skills") into the plugin +// directory (plugin/), replacing any existing tree. +// +// The pin in sync-skills-vendor.json is the single source of truth — +// there is no runtime override. To ship a different skill version, +// change the pin in a PR and commit the synced tree alongside it. + +import { promises as fs, createWriteStream } from "node:fs"; +import { Readable } from "node:stream"; +import { pipeline } from "node:stream/promises"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import { fileURLToPath } from "node:url"; + +// filesystem helpers +async function readJson(filePath) { + return JSON.parse(await fs.readFile(filePath, "utf8")); +} + +async function fileExists(filePath) { + try { await fs.access(filePath); return true; } catch { return false; } +} + +// download the upstream tarball + +// codeload.github.com serves any public repo's archive over HTTPS +// without auth, accepting a tag, branch, or commit SHA as the ref. +async function downloadTarball(repo, ref, destPath) { + const url = `https://codeload.github.com/${repo}/tar.gz/${encodeURIComponent(ref)}`; + const res = await fetch(url, { redirect: "follow" }); + if (!res.ok) throw new Error(`Could not download ${repo}@${ref} (HTTP ${res.status})`); + await pipeline(Readable.fromWeb(res.body), createWriteStream(destPath)); + console.log(` fetched ${url}`); +} + +// extract the tarball + +// Shells out to the system `tar` instead of pulling in an npm tar library — +// keeps the script zero-dependency. +// +// GitHub tarballs always have exactly one top-level directory whose +// name encodes the repo + commit. We return that path so the caller +// knows where to find the extracted tree. +async function extractTarball(tarballPath, intoDir) { + await fs.mkdir(intoDir, { recursive: true }); + const result = spawnSync("tar", ["-xzf", tarballPath, "-C", intoDir], { stdio: "inherit" }); + if (result.status !== 0) throw new Error(`tar exited with status ${result.status}`); + const [topLevel] = await fs.readdir(intoDir); + return path.join(intoDir, topLevel); +} + +// copy one path from the extracted tree into the plugin + +// Removes the destination first so we never end up with stale leftovers +// from a previous sync, then creates the destination's parent directory then copies. +async function copyPath(fromDir, toDir, relativePath) { + const from = path.join(fromDir, relativePath); + const to = path.join(toDir, relativePath); + if (!(await fileExists(from))) { + throw new Error(`path missing in upstream tarball: ${relativePath}`); + } + await fs.rm(to, { recursive: true, force: true }); + await fs.mkdir(path.dirname(to), { recursive: true }); + await fs.cp(from, to, { recursive: true }); + console.log(` ${relativePath} -> ${path.relative(process.cwd(), to)}`); +} + +// Sync this plugin: read sync-skills-vendor.json, download + extract + copy. +// +// Paths are resolved relative to the script itself rather than CWD, so +// the script works regardless of where it's invoked from. The plugin +// directory is plugin/ at the repo root (two levels up from +// .github/scripts/). +async function main() { + const scriptDir = path.dirname(fileURLToPath(import.meta.url)); + const pluginDir = path.resolve(scriptDir, "..", "..", "plugin"); + const vendorPath = path.join(scriptDir, "sync-skills-vendor.json"); + if (!(await fileExists(vendorPath))) { + throw new Error(`missing sync-skills-vendor.json at ${vendorPath}`); + } + + const { repo, pin, paths } = await readJson(vendorPath); + if (!repo || !pin || !Array.isArray(paths) || paths.length === 0) { + throw new Error(`${vendorPath} must define 'repo', 'pin' and a non-empty 'paths' array`); + } + + console.log(`--- ${repo} (ref: ${pin}) ---`); + + const workDir = await fs.mkdtemp(path.join(tmpdir(), "sync-skills-")); + try { + // `slug` is just a unique filename for this tarball + extract dir. + const slug = `${repo.replace("/", "-")}-${pin.replace(/[^A-Za-z0-9._-]/g, "_")}`; + const tarball = path.join(workDir, `${slug}.tar.gz`); + await downloadTarball(repo, pin, tarball); + const extracted = await extractTarball(tarball, path.join(workDir, slug)); + for (const rel of paths) await copyPath(extracted, pluginDir, rel); + } finally { + await fs.rm(workDir, { recursive: true, force: true }); + } + console.log("done."); +} + +await main(); diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..403744e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# IDE settings +.idea/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ada66e7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing to the JFrog VS Code Plugin + +Thank you for your interest in contributing! This project is maintained by JFrog and licensed under the [Apache License 2.0](LICENSE). + +## Contributor License Agreement (CLA) + +All contributors must sign the [JFrog CLA](https://jfrog.com/cla/) before contributions can be merged. A CLA check runs automatically on every pull request — follow the prompts to sign if you haven't already. + +## How to Contribute + +1. **Fork** the repository and create a feature branch from `main`. +2. Make your changes, ensuring they follow the existing code style and project conventions. +3. **Test** by loading the plugin from source — see [the README](README.md#installation) for setup instructions. +4. **Commit** with a clear, descriptive message. +5. Open a **pull request** against `main` with a summary of what changed and why. + +### Updating the vendored skills + +The `plugin/skills/` tree is vendored from [jfrog/jfrog-skills](https://github.com/jfrog/jfrog-skills) and committed to `main`. The full refresh flow lives in [`README.md`](README.md#updating-the-vendored-skills) and [`VENDOR.md`](VENDOR.md). + +## Pre-release checklist + +- [ ] Version bumped in [`plugin/.claude-plugin/plugin.json`](plugin/.claude-plugin/plugin.json) when the plugin changes. +- [ ] No secrets, credentials, or files under `**/local-cache/` committed. +- [ ] If the skill tree changed: `.github/scripts/sync-skills-vendor.json` `pin` matches the upstream tag the new tree was generated from. +- [ ] Smoke-test the plugin locally. + +## Reporting Issues + +Open a [GitHub issue](https://github.com/jfrog/vscode-plugin/issues) with: + +- A clear title and description of the problem. +- Steps to reproduce (if applicable). +- Expected vs. actual behavior. + +## Code Guidelines + +- Keep changes focused — one logical change per PR. +- Follow existing patterns and naming conventions in the codebase. +- Do not commit secrets, credentials, or API keys. +- Add copyright headers to new source files: + +``` +// Copyright (c) JFrog Ltd. 2026 +// Licensed under the Apache License, Version 2.0 +// https://www.apache.org/licenses/LICENSE-2.0 +``` + +## Code of Conduct + +Be respectful and constructive. We are committed to providing a welcoming and inclusive experience for everyone. + +## Questions? + +Reach out to the JFrog DevRel team at . diff --git a/README.md b/README.md index d4ed663..741d890 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ The JFrog plugin provides the following capabilities, grouped by component: | Component | Feature | Description | | --- | --- | --- | | **Hook** | Agent Guard | Copilot manage MCPs through the JFrog Agent Guard. Through it you can discover, install, configure, update, and remove MCP servers from the JFrog AI Catalog approved for your project, and authenticate to remote HTTP MCPs via OAuth, API key, or bearer token. | +| **Skills** | JFrog Platform skills | Bundled [JFrog skills](https://github.com/jfrog/jfrog-skills) that teach Copilot how to interact with the JFrog Platform via the JFrog CLI, JFrog MCP server, and REST/GraphQL APIs — including a package safety and download workflow that consults the JFrog Public Catalog and curation policy before fetching packages. | --- @@ -137,6 +138,25 @@ See the [JFrog MCP Registry troubleshooting guide](https://docs.jfrog.com/ai-ml/ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for development setup, coding conventions, and the pull-request process. +## Updating the vendored skills + +The `plugin/skills/` tree is vendored from [`jfrog/jfrog-skills`](https://github.com/jfrog/jfrog-skills) at the version pinned in [`.github/scripts/sync-skills-vendor.json`](.github/scripts/sync-skills-vendor.json). To pull a newer upstream release into this repo: + +1. Bump `pin` in `.github/scripts/sync-skills-vendor.json` to the new tag (e.g. `v0.12.0`). +2. Run the sync script from the repo root: + + ```bash + node .github/scripts/sync-skills.mjs + ``` + + It downloads the pinned tarball from `codeload.github.com`, extracts it, and replaces the directories listed in `paths` (today: `plugin/skills/`). +3. Bump `version` in [`plugin/.claude-plugin/plugin.json`](plugin/.claude-plugin/plugin.json) so users actually receive the update — Claude Code skips installs whose resolved version hasn't changed. +4. Commit the pin bump, the regenerated `plugin/skills/` tree, and the version bump together, and open a PR. + +See [`VENDOR.md`](VENDOR.md) for the full picture. + +--- + ## Security See [`SECURITY.md`](SECURITY.md) for how to report vulnerabilities. diff --git a/VENDOR.md b/VENDOR.md new file mode 100644 index 0000000..8f13658 --- /dev/null +++ b/VENDOR.md @@ -0,0 +1,26 @@ +# Vendored skills + +The skill packages under `plugin/skills/` are vendored from **[jfrog/jfrog-skills](https://github.com/jfrog/jfrog-skills)** and committed to `main`. + +| | | +| --- | --- | +| **Repository** | https://github.com/jfrog/jfrog-skills | +| **Pinned release** | see `pin` in [`.github/scripts/sync-skills-vendor.json`](.github/scripts/sync-skills-vendor.json) | + +Included directories: `jfrog/`, `jfrog-package-safety-and-download/` (as of the pinned release). + +## Refreshing + +When the upstream repo publishes a new release, refresh the vendored tree via a PR that: + +1. Bumps `pin` in [`.github/scripts/sync-skills-vendor.json`](.github/scripts/sync-skills-vendor.json) to the new tag. +2. Re-syncs and commits the refreshed `plugin/skills/` tree. +3. Bumps `version` in [`plugin/.claude-plugin/plugin.json`](plugin/.claude-plugin/plugin.json) so users actually receive the update (Claude Code/Copilot skip installs whose resolved version hasn't changed). + +To regenerate the tree locally before opening the PR: + +```bash +node .github/scripts/sync-skills.mjs +``` + +The script reads `.github/scripts/sync-skills-vendor.json`, downloads the pinned upstream tarball from `codeload.github.com`, and replaces the directories listed in `paths` (today: `plugin/skills/`). diff --git a/marketplace.json b/marketplace.json index 5980571..09631a1 100644 --- a/marketplace.json +++ b/marketplace.json @@ -9,7 +9,6 @@ { "name": "jfrog", "description": "JFrog Platform integration with MCP, security skills, and supply-chain best practices", - "version": "1.0.1", "source": "plugin", "categories": ["security", "artifact-management", "supply-chain", "devops", "mcp", "mlops", "agent-guard", "ai-catalog"], "platforms": ["darwin", "linux", "windows"], diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 0c70bb8..98739ba 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "jfrog", "description": "JFrog Platform integration with MCP, security skills, and supply-chain best practices", - "version": "0.1.0", + "version": "0.2.0", "author": { "name": "JFrog", "url": "https://jfrog.com" }, "hooks": "hooks/hooks.json" } diff --git a/plugin/skills/jfrog-package-safety-and-download/SKILL.md b/plugin/skills/jfrog-package-safety-and-download/SKILL.md new file mode 100644 index 0000000..598318a --- /dev/null +++ b/plugin/skills/jfrog-package-safety-and-download/SKILL.md @@ -0,0 +1,286 @@ +--- +name: jfrog-package-safety-and-download +description: >- + Check JFrog Public Catalog and stored packages for a version, interpret + catalog security signals, and download through Artifactory (JFrog Platform + locations, remote cache, curation-aware package managers, or repo proxy). + Use when the user asks whether a package is safe, allowed, curated, or + wants to download npm, Maven, PyPI, Go, or similar packages via JFrog. + Do NOT use for pure CVE or vulnerability lookups (e.g. "details on + CVE-2021-23337") — those are handled by the jfrog skill's Public security + domain queries without this workflow. +metadata: + role: workflow +--- + +# JFrog Package Safety and Download + +## Prerequisites + +- Read `../jfrog/SKILL.md` for JFrog Platform concepts, domain model, CLI setup, and API patterns. +- **OneModel shapes drift by server version.** Before inventing GraphQL fields or `where` filters, read `../jfrog/references/onemodel-graphql.md` (schema fetch workflow) and `../jfrog/references/onemodel-query-examples.md` (**Public packages**, **Stored packages**). Regenerate or verify queries against `GET "$JFROG_URL/onemodel/api/v1/supergraph/schema"` when examples fail validation. + +## Workflow + +# Package safety check and download workflow + +When to read this file: + +- User asks to **check if a package is safe** and/or **download** it. +- User asks to **download a package** from Artifactory. +- User mentions checking a package for **curation** approval. +- User wants to know if a package is **allowed** or **approved** for use. + +## Workflow overview + +```mermaid +flowchart TD + A[User requests package check / download] --> B{Package in Public Catalog?} + B -->|Yes| C[Get latest version from Catalog] + B -->|No| D{Package in JFrog Platform Stored Packages?} + D -->|Yes| E[Get latest version from Stored Packages] + D -->|No| F[Package not found — stop] + C --> G{Latest version in JFrog Platform?} + E --> G + G -->|Yes| H[Safe — download from JFrog Platform] + G -->|No| I{Curation entitled?} + I -->|Yes| J[Check curation policy via API] + I -->|No| K[Download via remote repo] + J -->|200 Allowed| K + J -->|403 Blocked| M[Report curation blocked — stop] +``` + +### Parallelization opportunities + +Several steps in this workflow are independent and can run in parallel to +reduce total latency: + +- **Step 1 + Step 1 fallback**: When package type is known, query both the + Public Catalog (`getPackage`) and Stored Packages (`getPackage`) in + parallel. Use whichever returns data; if the Public Catalog returns a hit, + prefer its `latestVersion` for Step 2. +- **Step 3 + Step 5**: After determining the version, query stored package + versions (JFrog Platform check) and curation entitlement + (`/api/system/version`) in parallel. Both are independent reads — the + curation result is needed immediately if the JFrog Platform check returns + empty. + +When issuing parallel Shell calls, each `jf api` call authenticates +independently against the active `jf config` server; no shell state needs +to be passed between calls. + +## Step 1: Find the package + +Search the **Public Catalog** first via OneModel GraphQL, then fall back to +**Stored Packages** if not found. + +Execute the query through `jf api` as described in +`../jfrog/references/onemodel-graphql.md`; refer to +`../jfrog/references/onemodel-query-examples.md` for concrete query shapes. + +**When package type is known** (e.g. `npm`, `maven`, `pypi`), use +`publicPackages.getPackage(type:, name:)` (see *Get a public package*). +Include the `latestVersion { version }` selection set — `latestVersion` is +an object, not a scalar. + +**When type is unknown**, use `publicPackages.searchPackages` with +`nameContains` (see *Search public packages*). Add `type:` when the user +narrows the ecosystem. + +- **Found** → note `type` and `latestVersion.version`. Proceed to Step 2. +- **Not found** → the package may be 1st/2nd party. Search **Stored Packages** + using `storedPackages.searchPackages` or `storedPackages.getPackage` (see + *Stored packages domain* in `onemodel-query-examples.md`). Prefer + filtering by `type` when known; if not, use `nameContains` alone. + - **Found** → note `type` and `latestVersionName` (or derive a version from + `versionsConnection`). Proceed to Step 2. + - **Not found in either** → report "package not found" and stop. + +If multiple results with different `type` values, ask the user which package +type they mean. + +## Step 2: Determine latest version + +| Source | Version field | +|--------|--------------| +| Public Catalog | `latestVersion.version` (object selection required) | +| JFrog Platform Stored Packages | `latestVersionName` on `StoredPackage`, or highest entry from `versionsConnection` | + +## Step 3: Check if package + latest version exists in JFrog Platform + +Query stored package versions using `storedPackages.searchPackageVersions` +with a `hasPackageWith` filter (see `../jfrog/references/onemodel-query-examples.md` +→ *Search stored package versions*). Add a `version` filter for the specific +version from Step 2, and request `locationsConnection` to get repository +details (`repositoryKey`, `repositoryType`, `leadArtifactPath`). + +Execute the query through `jf api` (see +`../jfrog/references/onemodel-graphql.md` for the invocation pattern). + +- **Found with locations** → package is in the JFrog Platform. Report as **safe to + download**. Proceed to Step 4. +- **Not found** → proceed to Step 5. + +## Step 4: Download from JFrog Platform + +Use the location info from Step 3. Binary artifact downloads go through +`jf rt dl` — **not** `jf api`. `jf api` is the unified entry point for the +JFrog REST APIs (metadata, admin, curation, etc.) and does not expose the +`-L` / `-o` flags needed to stream binary content through a redirect chain. + +**`` must be a full file path** (e.g. +`./downloads/lodash-4.18.1.tgz`), not a bare directory. `jf rt dl --flat` +treats the target as a file name; passing a directory causes a misleading +"open path: is a directory" error. + +| `repositoryType` | Strategy | +|-------------------|----------| +| `local` or `federated` | `jf rt dl "/" --flat` | +| `remote` | `jf rt dl` against the **base** remote repo (strip any trailing `-cache`) — it transparently triggers the remote fetch when the artifact is not yet cached | + +**local / federated / remote download:** + +```bash +jf rt dl "/" --flat +``` + +**Resolving the remote repo key:** The `repositoryKey` returned by OneModel +for remote locations often already ends in `-cache` (e.g. +`devNPM-remote-cache`). `jf rt dl` needs the **base remote repo name** +(without `-cache`). Strip the `-cache` suffix when present (e.g. +`devNPM-remote-cache` → `devNPM-remote`). If the key does not end in +`-cache`, use it as-is. + +See the **Protocol endpoints** table below for the package-type-specific +path format inside the repo. + +## Step 5: Check curation entitlement + +```bash +jf api /artifactory/api/system/version \ + | jq '.addons | index("curation") != null' +``` + +- `true` → curation is entitled. Proceed to Step 6a. +- `false` → curation not available. Proceed to Step 6b. + +## Step 6a: Check curation policy and download + +When curation is entitled, use the Xray curation API to check whether the +package version is allowed across all repositories before downloading. + +```bash +RESPONSE_FILE="/tmp/curation-status-$$.json" +PAYLOAD_FILE="/tmp/curation-payload-$$.json" +STDERR_FILE="/tmp/curation-err-$$.log" + +jq -n \ + --arg type "" \ + --arg name "" \ + --arg version "" \ + '{packageType:$type, packageName:$name, packageVersion:$version}' \ + > "$PAYLOAD_FILE" + +set +e +jf api /xray/api/v1/curation/package_status/all_repos \ + -X POST -H "Content-Type: application/json" \ + --input "$PAYLOAD_FILE" \ + > "$RESPONSE_FILE" 2> "$STDERR_FILE" +RC=$? +set -e +echo "RC=$RC"; echo "$RESPONSE_FILE" +``` + +Supported `packageType` values: `npm`, `pypi`, `maven`, `go`, `nuget`, +`docker`, `gradle`. + +**Interpreting the result with `jf api`**: unlike plain `curl`, `jf api` +surfaces the HTTP result through its **exit code** and a +`" [Warn] ... returned 4xx/5xx"` line on **stderr** (not a +`%{http_code}` suffix in stdout). The response body is always written to +stdout. Parse both: + +```bash +if [ "$RC" -eq 0 ]; then + echo "Package is allowed by curation." +elif grep -q 'returned 403' "$STDERR_FILE"; then + echo "Blocked by curation policy:" + cat "$RESPONSE_FILE" +else + echo "Curation check failed (rc=$RC):" + cat "$STDERR_FILE" +fi +``` + +**Evaluate the outcome:** + +- **exit 0** → package is **allowed** by curation policy. Proceed to + download via a remote repo (same as Step 6b). +- **`returned 403` on stderr** → package is **blocked** by a curation + policy. The response body explains which policy rule blocked it. Report + the block reason to the user and stop — do not attempt to download. +- **Any other non-zero exit** → treat as an operational failure (auth, DNS, + endpoint disabled) and report. + +## Step 6b: Download without curation + +When curation is not entitled and the package is not in the JFrog Platform, +download directly through a remote repo. + +1. **Find a remote repo** of the right package type: + + ```bash + jf api \ + "/artifactory/api/repositories?type=remote&packageType=" \ + | jq '.[].key' + ``` + +2. **Download** — use `jf rt dl` against the base remote repo (without + `-cache`); it handles both cached and uncached artifacts: + + ```bash + jf rt dl "/" --flat + ``` + +## Artifact paths by package type + +Use these path patterns when `leadArtifactPath` is not available from +OneModel. The leading `/` is the base repo key you pass to `jf rt dl`. + +| Type | `jf rt dl` target pattern | +|--------|-------------------------------------------------------------------------| +| `npm` | `//-/-.tgz` | +| `pypi` | `///-.tar.gz` | +| `maven`| `////-.jar` | +| `go` | `//@v/.zip` | + +## Gotchas + +- **Binary downloads vs. `jf api`**: `jf api` is for REST APIs, not binary + content. It does not follow redirects transparently into a binary payload + and does not expose `-L` / `-o`. Always use `jf rt dl` (against the base + remote repo, not the `-cache` one) for the actual artifact download. +- **`jf rt dl` and uncached remotes**: `jf rt dl "/"` — + targeting the **base** remote repo rather than `-cache/` — + transparently triggers the remote fetch and caches the artifact. Do not + try to pre-query the proxy via `jf api`. +- **`jf rt dl --flat` target must be a file path**: When downloading a + single artifact, pass a full output **file** path (e.g. + `./downloads/lodash-4.18.1.tgz`), not a directory. The CLI opens the target + path as a file; a directory causes a cryptic "open path: is a directory" + error that retries four times before failing. Derive the filename from + `leadArtifactPath` (take the segment after the last `/`). +- **Package type detection**: If the user doesn't specify the package type, + the Public Catalog search by name alone may return multiple types. Ask the + user to disambiguate before proceeding. +- **Curation endpoint lives under Xray**: use + `/xray/api/v1/curation/package_status/all_repos` (via `jf api`). Do not + prefix it with `/artifactory`. +- **Curation result discrimination with `jf api`**: the 200/403 signal comes + from `jf api`'s **exit code** plus a `returned NNN` line on **stderr**, + not from a `%{http_code}` appended to stdout. Capture stderr to a file + (`2> "$STDERR_FILE"`) and branch on `RC` + `grep 'returned 403'` as shown + in Step 6a. +- **Curation API package type values**: Must be lowercase and match one of + `npm`, `pypi`, `maven`, `go`, `nuget`, `docker`, `gradle`. Other values + will return an error. diff --git a/plugin/skills/jfrog/SKILL.md b/plugin/skills/jfrog/SKILL.md new file mode 100644 index 0000000..67e0391 --- /dev/null +++ b/plugin/skills/jfrog/SKILL.md @@ -0,0 +1,529 @@ +--- +name: jfrog +description: >- + Interact with the JFrog Platform via the JFrog CLI, JFrog MCP server and REST/GraphQL APIs. + Use this skill when the user wants to manage Artifactory repositories, + upload or download artifacts, manage builds, configure permissions, + manage users and groups, work with access tokens, configure JFrog CLI + servers, search artifacts, manage properties, set up replication, + manage JFrog Projects, run security audits or scans, look up CVE details, + query exposures scan results from JFrog Advanced Security, manage + release bundles and lifecycle operations, aggregate or export platform + data, or perform any JFrog Platform administration task. + Also use when the user mentions jf, jfrog, artifactory, xray, distribution, + evidence, apptrust, onemodel, graphql, workers, mission control, curation, + advanced security, exposures, or any JFrog product name. +compatibility: >- + Requires jq on PATH. +metadata: + role: base + version: "0.11.0" +--- + +# JFrog Skill + +The foundational skill for all JFrog agent interactions. Covers JFrog Platform concepts, `jf` CLI setup and authentication, and intent routing to workflow skills. + +Interact with the JFrog Platform through three tool tiers — see +[Tool selection strategy](#tool-selection-strategy). In code examples below, +`` refers to this skill's directory and is resolved automatically +by the agent. If the agent does not resolve it, determine the path by locating +this SKILL.md file and using its parent directory. + +## Tool selection strategy + +Try the tiers in order; move to the next only when the current does not +cover the operation or fails: + +1. **JFrog MCP tools** (preferred): `CallMcpTool` against the JFrog MCP + server. Discover available tools from the server's tool list; never + guess tool names. +2. **`jf` CLI subcommands** (fallback): dedicated commands such as + `jf rt upload`, `jf rt dl`, `jf build-publish`. +3. **`jf api`** (last resort): REST/GraphQL endpoints with no dedicated + subcommand. Validate the path first — see rule 6 in + [Cautious execution](#cautious-execution). + +MCP and the CLI may use different token scopes. If one tier returns 403, +try the alternate tier before reporting the operation blocked. + +## Prerequisites + +The following tools must be available on `PATH`: + +| Tool | Purpose | +|------|---------| +| `jq` | JSON parsing of CLI and API output | + +All JFrog HTTP traffic from Tiers 2 and 3 goes through the `jf` CLI itself +(`jf api`, see [Invoking platform APIs with `jf api`](#invoking-platform-apis-with-jf-api) below) — +no standalone `curl` is required for any JFrog interaction. + +**Runtime permission for JFrog calls.** All `jf` calls that touch the network +need an outbound-HTTPS escalation from the agent runtime. The `~/.jfrog/` +credential save (`jf config add` during login) additionally needs a +filesystem-write escalation. + +| Runtime | Network | Network + `~/.jfrog/` write | +| ----------- | --------------------------------------------- | ------------------------------- | +| Cursor | `required_permissions: ["full_network"]` | `required_permissions: ["all"]` | +| Claude Code | `allowed-tools: Bash(jf:*)` + host allowlist | same + filesystem allowlist | +| Other | Configure at the runtime/sandbox layer | same | + +If `jf` exits 1 with empty output, the runtime's network gate is the first +thing to check — re-run with the appropriate escalation above. + +## Environment check + +MCP (Tier 1) operations do not require this check and can proceed immediately. +Before your first Tier 2 or Tier 3 (`jf`) operation in a session, run the +environment check and **remember its stdout** as `` for the rest of the +session: + +```bash +bash /scripts/check-environment.sh +# stdout (one line): jfrog-skills/ [(tool=; model=)] jfrog-cli-go/ +# stderr: JSON state (cached 24h at ${JFROG_CLI_HOME_DIR:-$HOME/.jfrog}/skills-cache/jfrog-skill-state.json) +``` + +Pass the precise underlying-model slug with version: `opus-4.7`, +`sonnet-4.5`, `gpt-5-codex`, `gemini-2.5-pro`, `composer-2-fast`. Cursor's +Composer product slug **is** the canonical id — use it as-is. Do **not** +pass harness/role names (`subagent`, `agent`, `assistant`) or bare family +names (`claude`, `gpt`); subagents inherit the parent's slug. If genuinely +unknown, pass `unknown`. + +### Export `JFROG_CLI_USER_AGENT` once per bash invocation + +At the top of every bash invocation that runs `jf`, export `` once; +all `jf` calls in that invocation pick it up: + +```bash +export JFROG_CLI_USER_AGENT='' +jf config show +jf api /artifactory/api/system/version +``` + +Do **not** repeat the assignment per `jf` call (`JFROG_CLI_USER_AGENT='' jf …` +on every line). Examples elsewhere in this skill and in `references/*.md` +omit the export for readability — the rule is global. When launching a +subagent, pass `` in its prompt; subagents do not re-run the script. + +| Exit | Meaning | +|------|---------| +| 0 | Cache fresh — CLI ready (Tiers 2 and 3 available), proceed | +| 1 | Cache refreshed — CLI ready (Tiers 2 and 3 available), proceed | +| 2 | `jf` not installed — Tiers 2 and 3 unavailable; only MCP (Tier 1) remains | +| 3 | `jf` below minimum version — Tiers 2 and 3 unavailable; only MCP (Tier 1) remains | + +Exit 2 or 3 is not a fatal error. Attempt to install or upgrade the CLI +(see `references/jfrog-cli-install-upgrade.md`). If installation succeeds, +re-run the environment check. If installation is not possible (no permissions, +restricted environment), proceed with MCP (Tier 1) only. Both `jf` CLI commands +(Tier 2) and `jf api` (Tier 3) require a working `jf` installation. + +### JSON parsing (`jq`) + +Use **`jq`** for all JSON parsing of CLI and API output (pipes, `-r`, filters). + +## `~/.jfrog/skills-cache/` — allowed files only + +`${JFROG_CLI_HOME_DIR:-$HOME/.jfrog}/skills-cache/` is **not** a general scratch +or temp directory. Use it **only** for these two artifacts: + +1. **`jfrog-skill-state.json`** — written by `scripts/check-environment.sh` + (24-hour CLI check cache). +2. **`onemodel-schema-${JFROG_SERVER_ID}.graphql`** — cached OneModel supergraph + schema (see `references/onemodel-graphql.md`). + +**Do not** save HTTP response bodies, GraphQL query results, ad-hoc JSON, reports, +or any other temporary files under `skills-cache/`. Write those to a host temp +path instead (for example `/tmp/-$$.json` or `mktemp -d`), echo the path +when a follow-up Shell step must read the file — same pattern as *Preserving +command output* below. + +## Cautious execution + +Do not run commands speculatively. Before executing any JFrog CLI command, +MCP tool call, or API call: + +1. Confirm the operation is needed to fulfill the user's request. + If the request is ambiguous or could refer to multiple systems (e.g. + "builds" could mean Artifactory build-info or CI/CD pipeline runs), + **ask the user for clarification** instead of guessing. Never fetch data + from the wrong system — a wrong answer is worse than asking a question. +2. Resolve the target server using the **Server selection rules** below — + there must be no ambiguity about which server is used +3. For mutating operations (create, update, delete, upload), confirm with the + user unless the intent is clearly implied. This applies to all tiers + (MCP tools, CLI commands, and `jf api` with POST/PUT/DELETE). +4. Prefer read operations first to understand current state before making changes +5. **Never invent preparatory mutations.** If the requested operation fails + because a precondition is not met (artifact missing from the specified repo, + repository does not exist, package not at the expected location, build not + found), **stop and report the gap to the user**. Do not perform copy, move, + upload, create-repo, or any other mutating operation to satisfy the + precondition unless the user explicitly asks for it. These "helper" mutations + can have cascading effects the user has not considered — virtual repository + resolution changes, storage quota consumption, replication triggers, Xray + re-indexing, or permission propagation. +6. **Never guess tool names or API paths.** For MCP tools, confirm the tool + exists in the server's tool list. For `jf api` paths, validate against + `/references/` (or + [JFrog OpenAPI specifications](https://docs.jfrog.com/integrations/docs/openapi-specifications) + if you have web access). On a 404, stop and report — never retry with a guessed + alternative path. + +## Server selection rules (mandatory) + +**Single-server invariant.** Every `jf` call MUST pass `--server-id ` +(default resolved below); for one user request, all `jf` calls use **exactly +one** server-id. A wrong answer from the wrong server is worse than a stop-and-ask. + +**JFrog MCP and CLI use independent auth.** MCP tools authenticate through +the MCP server session (not `jf config`); CLI commands authenticate through +`jf config`. If you switch the CLI target server via `jf config use`, the +MCP connection still points to its original server. Do not mix MCP and CLI +calls targeting different servers in the same session. If the user asks to +switch servers, warn that MCP tools will continue to target the original +server until the MCP connection is re-established. + +**MUST NOT** retry on a second configured server after 401/403/404, empty, or +partial results; **MUST NOT** infer multi-server intent from "my"/"our" or +from seeing extra entries in `jf config show`. **Override:** only when the user +**explicitly** names another id ("on ``, …", "use ``", "compare `` +and ``") — inferred intent is not an override. + +### Resolve the default once per session + +Before your first `jf` call, resolve the default server-id and **remember it** +as `` for the rest of the session, same pattern as ``: + +```bash +jf config show 2>/dev/null \ + | awk '/^Server ID:/{id=$NF} /^Default:[[:space:]]*true/{print id; exit}' +# stdout: the default server-id; if empty, stop and ask which to use +``` + +Pass `--server-id ` to every subsequent `jf` call. The flag goes +**after** the subcommand name, not after `jf` itself: + +- ✅ `jf api --server-id /artifactory/api/system/version` +- ✅ `jf rt ping --server-id ` +- ❌ `jf --server-id api /…` — fails with `flag provided but not defined` + +When launching a subagent, pass `` in its prompt — subagents do not +re-resolve. Examples elsewhere in this skill and in `references/*.md` omit +`--server-id` for readability; the rule is global, same as +`JFROG_CLI_USER_AGENT`. To add a new server, read +`references/jfrog-login-flow.md`. + +### On any error, stop — never switch + +If a `jf` call returns 401/403, 404, network error, timeout, or any other +failure, **stop with no further `jf` calls** and respond: + +> `` returned `` for ``: ``. Other +> configured server(s): `` — I won't query them without your explicit +> instruction. How would you like to proceed? + +## When to read reference files + +Load the most specific file for the task at hand. Avoid loading more than 2-3 +reference files for a single operation — start with the most relevant one and +only load additional files if the first doesn't cover the need. File sizes +vary (~25–640 lines); larger files are noted with approximate line counts +below. + +### Cross-domain + +- **Disambiguating a JFrog entity, understanding entity types, or planning operations that span multiple products**: read `references/jfrog-entity-index.md`, then follow pointers to the relevant domain file +- **Looking up documentation URLs**: read `references/jfrog-url-references.md` + +### Artifactory + +- **Repository types, artifacts, builds, properties, or permission targets (concepts)**: read `references/artifactory-entities.md` (~220 lines) +- **Stored packages, package versions, version locations, or the metadata layer over Artifactory (concepts)**: read `references/stored-packages-entities.md` (~165 lines) +- **Repo, file, build, permission, user/group, or replication operations**: if the JFrog MCP server exposes a tool for the operation, prefer it. For CLI/API fallback, read `references/artifactory-operations.md` (for **listing builds** use AQL with `limit`/`offset` — see § *Listing build names*; for **full build detail** use `GET /api/build//?project=` — see § *Retrieving full build info*) +- **AQL queries**: read `references/artifactory-aql-syntax.md` (~585 lines) +- **Artifactory REST beyond the CLI, structured JSON templates (replacing interactive wizards), or any Artifactory API gap**: read `references/artifactory-api-gaps.md` (~220 lines) + +### Xray & security + +- **Watches, policies, violations, components, or vulnerability scanning (concepts)**: read `references/xray-entities.md` (~290 lines) +- **Exposures scanning results (secrets, IaC, service misconfigurations, application security risks)**: read `references/xray-entities.md` § Exposures (Advanced Security) +- **Curation audit events (approved/blocked packages, dry-run policy evaluations, curation export)**: read `references/xray-entities.md` § Curation audit events + +### Release lifecycle & distribution + +- **Release bundles, lifecycle stages, distribution, or evidence (concepts)**: read `references/release-lifecycle-entities.md` (~180 lines) +- **Applications, application versions, releasables, promotions, or AppTrust (concepts)**: read `references/apptrust-entities.md` (~155 lines) + +### Catalog + +- **Public or custom catalog, package metadata, vulnerability advisories, licenses, OpenSSF, or MCP services (concepts)**: if the JFrog MCP server exposes a catalog tool, prefer it for single-package lookups. For deeper queries, read `references/catalog-entities.md` (~190 lines) +- **CVE details, vulnerability lookup by CVE ID, or severity/affected-packages/fix-versions for a specific CVE**: prefer an MCP vulnerability-lookup tool if the JFrog MCP server exposes one. Otherwise read `references/onemodel-query-examples.md` § *Public security domain* for the `searchVulnerabilities` query shape — this is self-contained; do not load the `jfrog-package-safety-and-download` skill for pure CVE lookups + +### OneModel (GraphQL) + +- **GraphQL queries** (applications, packages, evidence, release bundles, catalog, cross-domain, or "list/search my" platform entities): read `references/onemodel-graphql.md` (~325 lines) +- **Query templates and domain-specific examples**: read `references/onemodel-query-examples.md` (~555 lines) +- **Pagination, filtering, GraphQL variables, or date formatting**: read `references/onemodel-common-patterns.md` (~280 lines) + +### Platform administration + +- **Platform structure, project/repo membership, or project roles vs environments (concepts)**: read `references/platform-access-entities.md` +- **Access tokens, stats, projects, or system health**: read `references/platform-admin-operations.md` +- **Managing JFrog Projects, members, or environments**: read `references/projects-api.md` (~260 lines) +- **Platform REST beyond the CLI, or any platform-level API gap**: read `references/platform-admin-api-gaps.md` (~180 lines) + +### CLI setup & authentication + +- **Adding a server or logging in**: read `references/jfrog-login-flow.md` (~130 lines) +- **CLI not installed, upgrade needed, or `jq` unavailable**: read `references/jfrog-cli-install-upgrade.md` + +### General patterns + +- **Batching, parallel Shell calls, or launching subagents**: read `references/general-parallel-execution.md` (~135 lines) +- **Large or parallel data gathering, list-vs-detail APIs, cache hygiene**: read `references/general-bulk-operations-and-agent-patterns.md` +- **Standalone HTML report with JFrog-aligned styling**: read `references/jfrog-brand-html-report.md` +- **Reusable gotchas from past tasks**: read or extend `references/general-use-case-hints.md` + +## Command discovery + +Use the commands listed below as your primary reference. Run `--help` to +verify options you are unsure about or to discover commands not listed here — +do not rely on memorized commands outside this skill, as they may be outdated. + +1. `jf --help` — list all namespaces and top-level commands +2. `jf --help` — list subcommands in a namespace +3. `jf --help` — show usage, arguments, and options + +### CLI namespaces + +| Namespace | Alias | Product | +|-----------|-------|---------| +| `rt` | | Artifactory | +| `xr` | | Xray | +| `ds` | | Distribution V1 | +| `at` | `apptrust` | AppTrust | +| `evd` | | Evidence | +| `mc` | | Mission Control | +| `worker` | | Workers | +| `config` | `c` | CLI server configuration | +| `plugin` | | CLI plugin management | +| `ide` | | IDE integration | + +> **Sunset notice:** JFrog Pipelines has been sunset and is no longer supported. +> Do not use the `pl` CLI namespace or the Pipelines REST API +> (`/pipelines/api/...`). If a user asks about Pipelines, inform them the +> product has been sunset. + +Top-level lifecycle commands (no namespace): `rbc`, `rbp`, `rbd`, `rba`, +`rbf`, `rbe`, `rbi`, `rbs`, `rbu`, `rbdell`, `rbdelr`. + +Top-level security commands: `audit`, `scan`, `build-scan`, `curation-audit`, +`sbom-enrich`. + +Top-level other: `access-token-create` (`atc`), `login`, `how`, `stats`, +`generate-summary-markdown`, `exchange-oidc-token`, `completion`. + +## Invoking platform APIs with `jf api` + +`jf api` is the Tier 3 entry point for JFrog Platform REST and GraphQL +endpoints, auto-authenticated against the resolved server. **Do not use +`jf rt curl` or `jf xr curl`**; they are superseded by `jf api`. + +### Product-prefix table + +`jf api` requires the **full** path including the product prefix; omitting it +returns 404. + +| Product | Path prefix | +|---------|-------------| +| Artifactory | `/artifactory/api/...` | +| Xray | `/xray/api/...` | +| Access (users, groups, tokens, permissions, projects) | `/access/api/...` | +| Evidence | `/evidence/api/...` | +| Release Lifecycle | `/lifecycle/api/...` | +| AppTrust | `/apptrust/api/...` | +| Distribution | `/distribution/api/...` | +| OneModel (GraphQL) | `/onemodel/api/v1/graphql`, `/onemodel/api/v1/supergraph/schema` | +| Mission Control | `/mc/api/...` | +| Curation | `/xray/api/v1/curation/...` (lives under Xray) | + +### Examples + +```bash +jf api /artifactory/api/repositories +jf api --server-id /artifactory/api/system/version + +# AQL (POST with text/plain body) +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" -d '' +``` + +Common flags: `-X/--method`, `-H/--header`, `-d/--data`, `--input `, +`--server-id`, `--timeout`. Body on stdout, status on stderr — see +[Gotchas](#gotchas). + +### GraphQL (OneModel) + +OneModel is the unified GraphQL API. **Do not** embed the query inside a JSON +literal (`-d '{"query":"..."}'`) — escaping breaks requests. Build the payload +with `jq -n --arg`, pass it via `--input`, and save the response to a file +before running `jq` on it. + +```bash +QUERY='{ evidence { searchEvidence(first: 5, where: { hasSubjectWith: { repositoryKey: "my-repo-local" } }) { totalCount } } }' +PAYLOAD=/tmp/onemodel-payload-$$.json RESPONSE=/tmp/onemodel-$$.json +jq -n --arg q "$QUERY" '{query:$q}' > "$PAYLOAD" +jf api /onemodel/api/v1/graphql -X POST \ + -H "Content-Type: application/json" --input "$PAYLOAD" > "$RESPONSE" +jq . "$RESPONSE" +``` + +Schema discovery: `jf api /onemodel/api/v1/supergraph/schema > "$SCHEMA_FILE"` +(store only under `~/.jfrog/skills-cache/`, never query responses). Read +`references/onemodel-graphql.md` for the full workflow (schema fetch, +validation, pagination, errors), plus `references/onemodel-query-examples.md` +and `references/onemodel-common-patterns.md` for query shapes, pagination, +variables, and dates. + +## Structured inputs + +Several CLI commands require JSON template files. The templates are normally +created by interactive wizard commands (`jf rt rpt`, `jf rt ptt`, `jf rt rplt`) +which agents cannot use. Instead, retrieve an existing config via REST API as a +starting point and modify it: + +```bash +jf api /artifactory/api/repositories/ +``` + +For other Artifactory or platform REST patterns, or when you need more than +this repo GET, see **Any API gap** under [When to read reference files](#when-to-read-reference-files). + +## Gotchas + +### MCP tools + +- MCP tools return structured data in the tool result. Read response fields + directly; do not pipe MCP output through shell commands or `jq`. + +### CLI and `jf api` + +- `jf api` requires the **product prefix** in the path. Omitting it returns + 404. See the [product-prefix table](#product-prefix-table) for the full list. +- `jf api` writes the body (success or error JSON) to **stdout** and + `[Info] Http Status: NNN` to **stderr** on every call; non-2xx also exits + 1 and adds `[Warn] jf api: returned NNN`. Pipe stdout to + `jq` directly; **never `2>&1 | jq`** — stderr corrupts the JSON. To keep + diagnostics: `jf api 2>/tmp/err-$$.log | jq .`. +- `jf api` has **no `-L`** (follow redirects) and **no `-o`** (output file). + Save bodies with shell redirection + (`jf api ... > /tmp/out-$$.json`); for + binary downloads through the Artifactory remote proxy prefer `jf rt dl`, + which handles the cache and redirect semantics natively. +- Remote repository content is stored in a `-cache` suffixed repo. Properties + and AQL queries for remote repo artifacts must target the cache repo. + Conversely, `/api/repositories/` only accepts the parent remote key + (without `-cache`) — strip the suffix for configuration lookups. +- **Do not use `jf rt search`** — always use a direct AQL query via + `jf api /artifactory/api/search/aql -X POST -H "Content-Type: text/plain" -d ''`. + See `references/artifactory-aql-syntax.md`. +- Use `--quiet` flag for non-interactive execution (suppresses confirmation + prompts). **Caution:** `--quiet` is not a global flag — commands that do not + support it (e.g. `jf rt s`, `jf rt ping`) will fail with misleading errors + like "Wrong number of arguments" or "flag provided but not defined". Check + `--help` for a command before adding `--quiet`. +- Use `--server-id` when targeting a non-default server. If a command fails + with `--server-id`, do not retry without it — that silently targets the + default server instead. See [Server selection rules](#server-selection-rules-mandatory). +- Never use interactive commands. All JFrog CLI operations must be performed + non-interactively. Known interactive commands to avoid: `jf config add`, + `jf login`, `jf rt repo-template`, `jf rt permission-target-template`, and + `jf rt replication-template`. For server setup, follow `references/jfrog-login-flow.md`. + For templates, use JSON schemas or REST API. If a command prompts for input + unexpectedly, find the non-interactive alternative via `--help` or REST API. +- `jf config export` output is base64-encoded JSON. Decode with + `base64 -d | jq` to extract fields. +- Build info lookups require a scope (`?buildRepo=` or `?project=`) — + resolve it before calling the API. See `references/artifactory-operations.md` + §Retrieving build info for the full workflow. +- If a `jf api` call returns 401, the configured token may have expired or + been rotated — ask the user to re-run the login flow (see + `references/jfrog-login-flow.md`) for the **same** server. If 403, the + token lacks required permissions. If 404, verify the endpoint path + (especially the product prefix) and target server version. On any of + these errors, do not try a different configured server as a workaround — + that targets a different environment. Report the error and ask the user. +- **Xray contextual analysis:** the summary artifact response has two + applicability fields — `applicability` (top-level, often null) and + `applicability_details` (always present with a `result` string). **Use + `applicability_details[].result` for counts and summaries.** Using the + top-level `applicability` field for aggregation produces wrong counts because + it is null when no scanner exists. See `references/xray-entities.md` + §Contextual analysis for the eight possible result values and jq snippets. +- **OneModel GraphQL:** always fetch the supergraph schema from the **same** + server you query before building operations (schemas differ by deployment); + cache, validate, and execute per `references/onemodel-graphql.md`. +- Never duplicate a network-fetching command to retry `jq` parsing — save the + response to a temp file first (see [Preserving command output](#preserving-command-output)). +- When collecting detail responses in a loop (e.g. per-repo GETs), validate + each body with `jq -e .` before appending to a results file. One non-JSON + or empty response corrupts a downstream `jq -s` slurp. Write validated + lines to an NDJSON file, then `jq -s '.' file.ndjson` to produce the final + array. See `references/general-bulk-operations-and-agent-patterns.md`. +- Accumulated edge cases from real tasks live in `references/general-use-case-hints.md` + — read when debugging odd failures; **append** a short entry when you confirm + a new, reusable gotcha. + +## Batch and parallel execution + +When a task requires multiple independent operations, use the lightest +parallelism mechanism that fits. Three tiers: (1) batch commands in a single +Shell call using loops or `&`, (2) issue parallel Shell tool calls, (3) launch +parallel subagents for large fan-out. Read `references/general-parallel-execution.md` +(~135 lines) for tier selection, examples, and subagent prompt structuring. + +## Preserving command output + +When a CLI command or API call returns data, redirect the output to a temporary +file so you can re-read it without re-executing the call: + +```bash +OUT=/tmp/jf-repos-$$.json +jf api /artifactory/api/repositories > "$OUT" +echo "$OUT" +``` + +Use `$$` (the shell PID) in the filename to prevent collisions across +concurrent sessions or processes. + +**Cross-call gotcha:** each Shell tool invocation runs in a new process with a +different PID, so `$$` expands to a different value in each call. Always +**echo the expanded filename** so the agent can read it from the output and +reuse the literal path in subsequent calls. Three patterns, in priority order: + +1. **`$$` + echo** (preferred): use `$$` for collision safety, echo the path + as shown above. The agent reads `/tmp/jf-repos-12345.json` from the output + and passes that literal value to the next Shell call. +2. **Session ID**: when many files share a prefix across calls, generate an ID + once (`SID=$(date +%s)-$$`), echo it, and reuse in later calls. +3. **Hardcoded names**: last resort — risks collisions when parallel calls or + subagents write to the same path. + +This protects against wasted round-trips when you need to retry parsing — for +example, if a `jq` filter fails or you extract the wrong field on the first +attempt. Re-read the file instead of hitting the server again. + +Do **not** duplicate the same **network** request in a shell pipeline (e.g. with +`||`) only to re-run `jq` or to reveal jq diagnostics—the duplicate call +adds load on JFrog without fetching new data. Run +`jq '' /tmp/jf-*-$$.json` (or redirect stdin from the file) instead +of re-running the same `jf api` or other identical network-backed command. + +Do **not** reuse saved output across unrelated steps or changed contexts (different +server, user, or intent). The file is only valid for the immediate sequence of +operations that motivated the original call. diff --git a/plugin/skills/jfrog/assets/.gitkeep b/plugin/skills/jfrog/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugin/skills/jfrog/references/apptrust-entities.md b/plugin/skills/jfrog/references/apptrust-entities.md new file mode 100644 index 0000000..6ca5088 --- /dev/null +++ b/plugin/skills/jfrog/references/apptrust-entities.md @@ -0,0 +1,154 @@ +# AppTrust entities + +When to read this file: + +- Working with **applications**, **application versions**, or **releasables**. +- Querying or managing **application version promotions** through stages. +- Understanding what **sources** (builds, release bundles, other app versions) feed into an application version. +- Using the OneModel GraphQL API with the `applications` query root. + +AppTrust entities are accessed exclusively via the **OneModel GraphQL API** +(`/onemodel/api/v1/graphql`). There are no CLI commands for this domain. + +For the OneModel query workflow (credentials, schema fetch, validation, +execution), read `references/onemodel-graphql.md`. + +## Entity relationship overview + +```mermaid +erDiagram + Application ||--o{ ApplicationVersion : "has versions" + ApplicationVersion ||--o{ Releasable : "contains" + ApplicationVersion ||--o{ Promotion : "promoted through" + ApplicationVersion }o--o{ Source : "assembled from" + Releasable }o--o{ Source : "contributed by" + Releasable ||--o{ Artifact : "contains" + Releasable }o--o| StoredPackageVersionLocation : "located at" + Application }o--o{ Owner : "owned by" + Application }o--o{ Label : "tagged with" + ApplicationVersion }o--o| EvidenceSubject : "attested by" +``` + +## Application + +The top-level entity representing a software application registered in +AppTrust. Applications belong to a JFrog Project and serve as the +organizational container for tracking versions, ownership, and criticality. + +| Field | Description | +|-------|-------------| +| `key` | Unique identifier (referenced as `applicationKey` or `appKey` elsewhere) | +| `projectKey` | JFrog Project this application belongs to | +| `displayName` | Human-readable name | +| `criticality` | `unspecified`, `low`, `medium`, `high`, `critical` | +| `maturityLevel` | `unspecified`, `experimental`, `production`, `end_of_life` | +| `owners` | List of users or groups that own the application | +| `labels` | Key-value pairs for custom categorization | + +Query: `applications.getApplication(key: "...")` or +`applications.searchApplications(where: {...})`. + +## Application version + +A versioned instance of an application. Each version captures a specific set +of releasable artifacts, their sources, and a promotion history through +lifecycle stages. + +| Field | Description | +|-------|-------------| +| `application` | Parent application | +| `version` | Version identifier (semantic or custom) | +| `tag` | Optional tag | +| `status` | Processing status: `STARTED`, `FAILED`, `COMPLETED`, `DELETING` | +| `releaseStatus` | Release maturity: `PRE_RELEASE`, `RELEASED`, `TRUSTED_RELEASE` | +| `currentStageName` | Most recent stage the version has been promoted to (null if never promoted) | +| `createdBy`, `createdAt` | Audit fields | +| `evidenceSubject` | Evidence attestation anchor (shared across domains) | + +The `releaseStatus` field is distinct from `status`: `status` tracks the +version creation process, while `releaseStatus` tracks its release maturity. + +Query: `applications.getApplicationVersion(applicationKey: "...", version: "...")` +or `applications.searchApplicationVersions(where: {...})`. + +## Releasable + +A deployable unit within an application version — either a **package version** +or an individual **artifact**. + +| Field | Description | +|-------|-------------| +| `name` | Package name or artifact file name | +| `version` | Package version (empty for non-package artifacts) | +| `packageType` | Repository package type (docker, maven, generic, etc.) | +| `releasableType` | `artifact` or `package_version` | +| `sha256` | Leading file checksum (e.g. manifest for Docker images) | +| `totalSize` | Sum of all artifact sizes in bytes | +| `sources` | Sources that contributed to this releasable | +| `artifacts` | Individual files that make up the releasable | +| `packageVersionLocation` | Link to `StoredPackageVersionLocation` for package releasables | +| `vcsCommit` | VCS commit details (for AppTrust-bound package versions) | + +Releasables bridge the application model to the underlying Artifactory +storage. The `packageVersionLocation` field connects to the Stored Packages +domain (see `stored-packages-entities.md`). + +## Application version promotion + +Records the promotion of an application version from one stage to another. +All promotions are recorded including failed attempts. + +| Field | Description | +|-------|-------------| +| `sourceStageName` | Stage being promoted from (empty for first promotion) | +| `targetStageName` | Stage being promoted to | +| `status` | `SUBMITTED`, `STARTED`, `PENDING`, `COMPLETED`, `FAILED`, `REJECTED` | +| `createdBy`, `createdAt` | Who initiated and when | +| `artifacts` | Artifacts included in this promotion (repo + path) | +| `messages` | Error messages if the promotion failed | + +Promotions use the same environment/stage model as Release Bundle promotions +(see `release-lifecycle-entities.md`) but at the application level. + +## Sources + +Sources describe how releasables were assembled into an application version. +Four types exist: + +| Source type | Fields | Description | +|-------------|--------|-------------| +| **Build** | `name`, `number`, `startedAt`, `repositoryKey` | A CI/CD build that produced releasables | +| **ReleaseBundle** | `name`, `version` | A release bundle whose artifacts were included | +| **ApplicationVersion** | `applicationKey`, `version` | Another application version (composition) | +| **Direct** | (none) | Directly included without an associated build or bundle | + +Sources appear at both the application version level (all sources) and the +individual releasable level (sources for that specific releasable). + +## Artifacts (within application versions) + +Individual files within releasables. + +| Field | Description | +|-------|-------------| +| `filePath` | Path in the repository (excluding repo key) | +| `downloadPath` | Full path for downloading from a Release Bundle repository | +| `sha256` | Checksum | +| `size` | Size in bytes | +| `evidenceSubject` | Evidence attestation anchor | + +## Cross-domain connections + +AppTrust entities connect to other domains via the OneModel GraphQL API: + +- **Evidence** — `ApplicationVersion.evidenceSubject` and + `ApplicationVersionArtifact.evidenceSubject` link to the Evidence domain + via `EvidenceSubject.fullPath`. This allows querying evidence attached to + app versions and their artifacts. +- **Stored Packages** — `Releasable.packageVersionLocation` links to + `StoredPackageVersionLocation`, connecting the application model to where + packages physically reside in Artifactory. +- **Release Bundles** — source type `ReleaseBundle` references release bundle + name/version from the Release Lifecycle domain. +- **Builds** — source type `Build` references build-info records from + Artifactory. diff --git a/plugin/skills/jfrog/references/artifactory-api-gaps.md b/plugin/skills/jfrog/references/artifactory-api-gaps.md new file mode 100644 index 0000000..1533d8a --- /dev/null +++ b/plugin/skills/jfrog/references/artifactory-api-gaps.md @@ -0,0 +1,206 @@ +# Artifactory API Gaps + +Operations available through REST API but not through CLI commands. +Invoke them via `jf api [flags]` (authentication is handled +automatically against the active `jf config` server; see the base skill's +*Invoking platform APIs with `jf api`* section). + +## Repository management + +### Get repository configuration +```bash +jf api /artifactory/api/repositories/ +``` +Returns the full JSON configuration of a repository. Useful as a template +for creating similar repos. + +### List all repositories +```bash +jf api /artifactory/api/repositories +``` +Optional query params (combinable): `type` (one of `local`, `remote`, +`virtual`, `federated`), `packageType` (e.g. `docker`, `maven`, `npm`, +`pypi`, `generic`), `project`. Examples: +```bash +jf api "/artifactory/api/repositories?type=local" +jf api "/artifactory/api/repositories?packageType=docker" +jf api "/artifactory/api/repositories?type=remote&packageType=maven&project=my-project" +``` + +### Get repositories (v2) +```bash +jf api /artifactory/api/repositories/configurations +``` +Optional query params (combinable, comma-separated values allowed): +`repoType` (case-insensitive; one of `local`, `remote`, `virtual`, +`federated`) and `packageType` (e.g. `maven`, `docker`, `npm`). Note: +`repo_type` is silently ignored — the correct name is `repoType`. +Examples: +```bash +jf api "/artifactory/api/repositories/configurations?repoType=local" +jf api "/artifactory/api/repositories/configurations?packageType=maven" +jf api "/artifactory/api/repositories/configurations?repoType=local,remote&packageType=docker" +``` + +### Check if repository exists +```bash +jf api /artifactory/api/repositories/ -X HEAD +# 200 = exists, 400 = does not exist +``` + +## Storage and system + +### Get storage summary +```bash +jf api /artifactory/api/storageinfo +``` + +### Refresh storage summary +```bash +jf api /artifactory/api/storageinfo/calculate -X POST +``` + +### Get storage item info +```bash +jf api "/artifactory/api/storage//" +``` + +### System ping +```bash +jf api /artifactory/api/system/ping +``` + +### System version +```bash +jf api /artifactory/api/system/version +``` + +### System configuration +```bash +jf api /artifactory/api/system/configuration +``` + +## Search (beyond CLI) + +### AQL queries +```bash +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" \ + -d 'items.find({"repo":"my-repo","name":{"$match":"*.jar"}})' +``` + +For remote repository content, query the `-cache` suffixed repo: +```bash +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" \ + -d 'items.find({"repo":"my-remote-cache"})' +``` + +### Property search +```bash +jf api "/artifactory/api/search/prop?key=value&repos=my-repo" +``` + +### Checksum search +```bash +jf api "/artifactory/api/search/checksum?sha256=" +``` + +### GAVC search (Maven) +```bash +jf api "/artifactory/api/search/gavc?g=com.example&a=mylib&v=1.0" +``` + +## User and group management + +User and group operations are handled by the Access service. See +`platform-admin-api-gaps.md` (Users / Groups sections) for the full set. + +## Metadata calculation + +Trigger metadata recalculation for various package types: +```bash +# Maven +jf api /artifactory/api/maven/calculateMetaData/ -X POST + +# npm +jf api /artifactory/api/npm//reindex -X POST + +# Docker +# (automatic, no manual trigger) + +# PyPI +jf api /artifactory/api/pypi//reindex -X POST + +# Helm +jf api /artifactory/api/helm//reindex -X POST + +# Debian +jf api /artifactory/api/deb/reindex/ -X POST +``` + +## Trash can and garbage collection + +### Empty trash +```bash +jf api /artifactory/api/trash/empty -X POST +``` + +### Restore from trash +```bash +jf api "/artifactory/api/trash/restore//" -X POST +``` + +### Run garbage collection +```bash +jf api /artifactory/api/system/storage/gc -X POST +``` + +## Federated repositories (beyond basic CRUD) + +### Get federation status +```bash +jf api /artifactory/api/federation/status/ +``` + +### Trigger full sync +```bash +jf api "/artifactory/api/federation/fullSyncAll/" -X POST +``` + +## Build info (beyond CLI) + +### List builds (prefer scoped queries) + +**Unscoped** `GET /artifactory/api/build` (no query parameters) can **time +out** on busy instances. Prefer **project-scoped** or **repo-scoped** +listing, then detail GETs. Full flow: read `artifactory-operations.md` +§ *Listing builds when the project key is known*. + +```bash +# Project scope — build names (latest per name) +jf api "/artifactory/api/build?project=" + +# Project scope — all run numbers for one build name (response: buildsNumbers) +jf api "/artifactory/api/build/?project=" + +# Build-info repo scope — alternative when you know the repo key +jf api "/artifactory/api/build?buildRepo=" +``` + +### Get build info +```bash +# Default build-info repo only (no project / non-default repo) +jf api "/artifactory/api/build//" + +# Project or custom build-info repo +jf api "/artifactory/api/build//?project=" +jf api "/artifactory/api/build//?buildRepo=" +``` + +### Delete builds +```bash +jf api /artifactory/api/build/delete \ + -X POST -H "Content-Type: application/json" \ + -d '{"buildName":"my-build","buildNumbers":["1","2"]}' +``` diff --git a/plugin/skills/jfrog/references/artifactory-aql-syntax.md b/plugin/skills/jfrog/references/artifactory-aql-syntax.md new file mode 100644 index 0000000..b120275 --- /dev/null +++ b/plugin/skills/jfrog/references/artifactory-aql-syntax.md @@ -0,0 +1,656 @@ +# AQL (Artifactory Query Language) + +AQL queries are sent as POST requests with `Content-Type: text/plain`: + +```bash +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" -d '' +``` + +## Query structure + +``` +.find() + .include() + .sort() + .offset() + .limit() + .distinct() +``` + +Only `.find()` is required. The others are optional and chainable. +**The chain order above is enforced by the server.** `.include()` must come +before `.sort()`, `.sort()` before `.offset()`, etc. Putting them out of +order (e.g. `.sort()` before `.include()`) produces a parse error. + +**Mandatory include fields:** `items` requires `"repo","path","name"`; +`builds` requires `"name","number","repo"`. Always include these even when +you only need a subset — narrow results with `jq` post-query instead: + +``` +items.find({"name":"commons-lang3-3.12.0.jar"}) + .include("repo","path","name") + .distinct(true) +``` + +## Domains + +AQL has 13 queryable domains. Each domain represents a different entity type +and has its own set of fields. + + +| Domain | Query name | Description | +| -------------------- | ------------------- | ---------------------------------------------- | +| Items | `items` | Artifacts stored in repositories (most common) | +| Properties | `properties` | Key-value properties on items | +| Item infos | `item.infos` | Property modification metadata | +| Statistics | `stats` | Download statistics (local and remote) | +| Builds | `builds` | Build info records | +| Build modules | `modules` | Modules within a build | +| Build artifacts | `artifacts` | Artifacts produced by a build module | +| Build dependencies | `dependencies` | Dependencies consumed by a build module | +| Build properties | `build.properties` | Key-value properties on builds | +| Build promotions | `build.promotions` | Build promotion records | +| Module properties | `module.properties` | Key-value properties on build modules | +| Release bundles | `releases` | Release bundle records | +| Release bundle files | `release_artifacts` | Files within a release bundle | + + +## Domain relationships + +Domains connect through the following join paths. Cross-domain queries +traverse these links — fields from related domains can appear in criteria +and include clauses by prefixing the domain path. + +```mermaid +erDiagram + items ||--o{ properties : "has" + items ||--o| item_infos : "has" + items ||--o{ stats : "has" + items ||--o{ artifacts : "via checksum" + items ||--o{ dependencies : "via checksum" + items ||--o{ release_artifacts : "has" + artifacts }o--|| modules : "belongs to" + dependencies }o--|| modules : "belongs to" + modules }o--|| builds : "belongs to" + modules ||--o{ module_properties : "has" + builds ||--o{ build_properties : "has" + builds ||--o{ build_promotions : "has" + release_artifacts }o--|| releases : "belongs to" +``` + + + +**Key:** Items connect to build artifacts and dependencies through SHA-1 +checksum matching, not a direct key. This means a cross-domain query from +items to builds traverses: items → artifacts → modules → builds. + +### Cross-domain field paths + +To reference a field from a related domain, use dot-separated domain paths: + +``` +items.find({"artifact.module.build.name":"my-build"}) + .include("name","repo","path","artifact.module.build.number") +``` + +Common cross-domain paths from items: + +- `stat.downloads`, `stat.downloaded` — download statistics +- `property.key`, `property.value` — item properties +- `artifact.module.build.name` — build that produced the item +- `artifact.module.build.number` — build number + +From builds: + +- `module.artifact.name` — artifacts in build modules +- `module.dependency.name` — dependencies of build modules + +## Fields by domain + +Field types: `string`, `date`, `int`, `long`, `itemType` (`file`, `folder`, +or `any`). Fields marked "default" are returned without explicit `.include()`. + +### items + + +| Field | Type | Default | +| --------------- | -------- | ------- | +| `repo` | string | yes | +| `path` | string | yes | +| `name` | string | yes | +| `type` | itemType | yes | +| `size` | long | yes | +| `depth` | int | yes | +| `created` | date | yes | +| `created_by` | string | yes | +| `modified` | date | yes | +| `modified_by` | string | yes | +| `updated` | date | yes | +| `actual_md5` | string | no | +| `actual_sha1` | string | no | +| `sha256` | string | no | +| `original_md5` | string | no | +| `original_sha1` | string | no | + + +Computed field: `virtual_repos` — returns virtual repositories that include +the item's actual repository. Must use `.include("virtual_repos")` explicitly; +requires `repo`, `path`, `name` in the result set. + +### properties + + +| Field | Type | Default | +| ------- | ------ | ------- | +| `key` | string | yes | +| `value` | string | yes | + + +### stats + + +| Field | Type | Default | +| ---------------------- | ------ | ------- | +| `downloads` | int | yes | +| `downloaded` | date | yes | +| `downloaded_by` | string | yes | +| `remote_downloads` | int | yes | +| `remote_downloaded` | date | yes | +| `remote_downloaded_by` | string | yes | +| `remote_origin` | string | yes | +| `remote_path` | string | yes | + + +### item.infos + + +| Field | Type | Default | +| ------------------- | ------ | ------- | +| `props_modified` | date | yes | +| `props_modified_by` | string | yes | +| `props_md5` | string | yes | + + +### builds + + +| Field | Type | Default | +| ------------- | ------ | ------- | +| `url` | string | yes | +| `name` | string | yes | +| `number` | string | yes | +| `started` | date | yes | +| `created` | date | yes | +| `created_by` | string | yes | +| `modified` | date | yes | +| `modified_by` | string | yes | +| `repo` | string | no | + + +### modules + + +| Field | Type | Default | +| ------ | ------ | ------- | +| `name` | string | yes | + + +### artifacts + + +| Field | Type | Default | +| ------ | ------ | ------- | +| `name` | string | yes | +| `type` | string | yes | +| `sha1` | string | yes | +| `md5` | string | yes | + + +### dependencies + + +| Field | Type | Default | +| ------- | ------ | ------- | +| `name` | string | yes | +| `scope` | string | yes | +| `type` | string | yes | +| `sha1` | string | yes | +| `md5` | string | yes | + + +### build.properties + + +| Field | Type | Default | +| ------- | ------ | ------- | +| `key` | string | yes | +| `value` | string | yes | + + +### build.promotions + + +| Field | Type | Default | +| ------------ | ------ | ------- | +| `created` | date | yes | +| `created_by` | string | yes | +| `status` | string | yes | +| `repo` | string | yes | +| `comment` | string | yes | +| `user` | string | yes | + + +### module.properties + + +| Field | Type | Default | +| ------- | ------ | ------- | +| `key` | string | yes | +| `value` | string | yes | + + +### releases + + +| Field | Type | Default | +| -------------- | --------------------------- | ------- | +| `name` | string | yes | +| `version` | string | yes | +| `status` | string | yes | +| `created` | date | yes | +| `signature` | string | yes | +| `type` | string (`SOURCE`, `TARGET`) | yes | +| `storing_repo` | string | yes | + + +### release_artifacts + + +| Field | Type | Default | +| ------ | ------ | ------- | +| `path` | string | yes | + + +## Comparators + + +| Operator | Meaning | Example | +| ---------- | -------------------------------- | ------------------------------------ | +| `$eq` | Equals (default if omitted) | `{"type":"file"}` | +| `$ne` | Not equals | `{"type":{"$ne":"folder"}}` | +| `$eqic` | Equals, case-insensitive | `{"name":{"$eqic":"README.md"}}` | +| `$match` | Wildcard match (`*`, `?`) | `{"name":{"$match":"*.jar"}}` | +| `$matchic` | Wildcard match, case-insensitive | `{"name":{"$matchic":"*.JAR"}}` | +| `$nmatch` | Wildcard not-match | `{"name":{"$nmatch":"*-SNAPSHOT*"}}` | +| `$gt` | Greater than | `{"size":{"$gt":"1000000"}}` | +| `$gte` | Greater than or equal | `{"stat.downloads":{"$gte":"10"}}` | +| `$lt` | Less than | `{"size":{"$lt":"5000"}}` | +| `$lte` | Less than or equal | `{"modified":{"$lte":"2025-01-01"}}` | + + +### Boolean operators + + +| Operator | Description | +| -------- | ---------------------------------------------------------------------- | +| `$and` | All conditions must match (implicit when fields are at the same level) | +| `$or` | Any condition must match | + + +``` +items.find({"$and":[ + {"repo":"my-repo"}, + {"$or":[ + {"name":{"$match":"*.jar"}}, + {"name":{"$match":"*.war"}} + ]} +]}) +``` + +### Relative date comparators + +AQL supports relative date queries with `$last` and `$before`: + + +| Operator | Meaning | Example | +| --------- | ------------------------------------------------------- | ------------------------------- | +| `$last` | Within the last N period (equivalent to `$gt` from now) | `{"modified":{"$last":"7d"}}` | +| `$before` | Before the last N period (equivalent to `$lt` from now) | `{"created":{"$before":"3mo"}}` | + + +Supported units: `d` (days), `w` (weeks), `mo` (months), `y` (years), +`s` (seconds), `mi` (minutes), `ms` (milliseconds). + +### Multi-property AND + +To match items that have property A=1 **and** property B=2 (different +property rows), use `$and` with `@` shorthand: + +``` +items.find({"$and":[ + {"@build.name":"my-build"}, + {"@build.number":"42"} +]}) +``` + +AQL also documents a `$msp` (multi-set property) operator for this purpose, +but `$msp` is **unreliable in practice** — it returns 0 results on many +server versions even when matching items exist. Prefer `$and` with `@` +shorthand, which is verified to work correctly. + +## Date queries + +Dates use ISO 8601 format for absolute dates: + +``` +items.find({"modified":{"$gt":"2025-06-01T00:00:00.000Z"}}) +``` + +Or use relative dates (preferred — avoids hardcoding timestamps): + +``` +items.find({"modified":{"$last":"30d"}}) +items.find({"created":{"$before":"6mo"}}) +``` + +## Property queries + +Two equivalent syntaxes for property filtering: + +**`@key` shorthand** — concise, works for single property conditions: + +``` +items.find({"repo":"my-repo","@build.name":"my-build","type":"file"}) +``` + +**Explicit form** — `property.key`/`property.value` pairs: + +``` +items.find({ + "repo":"my-repo", + "property.key":"build.name", + "property.value":"my-build" +}) +``` + +**Multi-property AND** — use `$and` with `@` shorthand to match across +different property rows: + +``` +items.find({"$and":[ + {"@build.name":"my-build"}, + {"@build.number":"42"} +]}) +``` + +> **Note:** The `@key` shorthand works inside `$and`. For `$or`, use the +> explicit `property.key`/`property.value` form if the shorthand does not +> return expected results. + +## Include + +Select which fields to return. Without `.include()`, AQL returns each +domain's default field set. + +**When you use `.include()`, you replace the defaults — so you must +explicitly list any required fields:** + +- `items` domain: always include `"repo","path","name"` (server rejects +the query otherwise) +- `builds` domain: always include `"name","number","repo"` + +``` +items.find({"repo":"my-repo"}) + .include("name","repo","path","size","sha256","stat.downloads") +``` + +Cross-domain includes use dot-separated paths: + +``` +items.find({"repo":"my-repo"}) + .include("name","repo","path","property.key","property.value") +``` + +## Sort and pagination + +``` +items.find({"repo":"my-repo"}) + .sort({"$desc":["modified"]}) + .offset(0) + .limit(50) +``` + +Sort directions: `$asc`, `$desc`. Sort fields must also appear in the result +set (explicit `.include()` or default fields). See +[Before constructing a query](#before-constructing-a-query) for sort +performance rules. + +## Distinct + +Deduplicate result rows: + +``` +items.find({"repo":"my-repo"}).distinct(true) +``` + +## Validation rules + +The server enforces these constraints — violating them produces an error: + +**Non-admin users:** + +- `items` domain queries must include `repo`, `path`, `name` in results +(needed for permission filtering) +- `builds` domain queries must include `name`, `number`, `repo` in results + +**Transitive mode** (`.transitive()` for querying through virtual repos): + +- Only works with `items` domain +- Include subdomains limited to `items` and `properties` +- Repo criteria must use `$eq` (exact match) with a single repository +- No `offset` or `sort` allowed + +## Before constructing a query + +Run through these checks before writing any AQL query: + +1. **Never `.sort()` without a `repo` filter** — forces a full table scan + across all repositories. Sort client-side with `jq` instead. Also, + `.sort()` on cross-domain fields (e.g. `stat.downloads` in `items.find()`) + is silently ignored — fetch all rows and sort client-side. +2. **Always set `.limit()`** — no built-in default limit; unbounded queries + can time out or OOM. Broad queries without a `repo` filter are especially + expensive. +3. **`range.total` = returned count, not total matching** — AQL has no + count-only mode. To find the true total, paginate with `.offset()` until + a page returns fewer results than the limit. +4. **AQL has no repo-type field** — to restrict to local repos, either + pre-query `GET /api/repositories?type=local` and add repo names to + criteria (practical when count is small), or query without a repo filter + and exclude `-cache` / `-virtual` suffixed repos client-side with `jq`. +5. **Narrow server-side first** — add every applicable filter (`created_by`, + `created`, `type`, `name`) before relying on client-side `jq` filtering. + +## Common query patterns + +### Find all JARs in a repo + +``` +items.find({"repo":"libs-release","name":{"$match":"*.jar"}}) +``` + +### Find large files (> 100 MB) + +``` +items.find({"repo":"my-repo","size":{"$gt":"104857600"},"type":"file"}) +``` + +### Find Maven SNAPSHOT JARs + +Use `*-SNAPSHOT*.jar` (not `*-SNAPSHOT.jar`) to also match classifier +artifacts like `-sources.jar` and `-javadoc.jar`: + +``` +items.find({"repo":"libs-snapshot","name":{"$match":"*-SNAPSHOT*.jar"},"type":"file"}) +``` + +### Find artifacts modified in the last 7 days + +``` +items.find({"repo":"my-repo","modified":{"$last":"7d"},"type":"file"}) + .sort({"$desc":["modified"]}) + .limit(100) +``` + +### Docker queries + +Use `"name":"manifest.json"` to **list tags** (one per tag). Use +`"name":{"$match":"*manifest.json"}` to **query all manifests** (includes +`list.manifest.json` for multi-arch tags — see [Gotchas](#gotchas)). + +``` +items.find({"repo":"docker-local","path":{"$match":"my-image/*"},"name":"manifest.json"}) +``` + +### Docker image size + +**Do not use AQL** — layer blobs live at `/sha256:/`, not +under `//`. Use the V2 manifest API (returns `layers[].size`): + +```bash +jf api "/artifactory/api/docker//v2//manifests/" \ + -H "Accept: application/vnd.docker.distribution.manifest.v2+json" +``` + +For multi-arch images the response is an image index; fetch each platform +manifest by digest to get its layers. + +### Find artifacts with a specific property + +``` +items.find({"repo":"my-repo","@build.name":"my-build","type":"file"}) +``` + +### Find never-downloaded files (zero download count) + +Zero-download items lack a stats row — filter client-side instead +(see [Gotchas](#gotchas)): + +```bash +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" -d ' +items.find({"repo":"my-repo","type":"file"}) + .include("repo","path","name","size","stat.downloads") +' | jq '[.results[] | select((.stats[0].downloads // 0) == 0) | {repo, path, name, size}]' +``` + +### Find artifacts not downloaded in 90 days + +Only matches previously-downloaded items (see [Gotchas](#gotchas)). +Combine with the never-downloaded pattern above for full coverage. + +``` +items.find({ + "repo":"my-repo", + "type":"file", + "stat.downloaded":{"$before":"90d"} +}).include("name","repo","path","stat.downloaded","size") +``` + +### Find items by build name (cross-domain) + +``` +items.find({"artifact.module.build.name":"my-service"}) + .include("name","repo","path","artifact.module.build.number") + .sort({"$desc":["modified"]}) + .limit(50) +``` + +### Find builds by name + +Non-admin users must include `name`, `number`, `repo` — omitting any +produces an error. + +``` +builds.find({"name":{"$match":"*my-service*"}}) + .include("name","number","repo","started") + .sort({"$desc":["started"]}) + .limit(10) +``` + +### Find build artifacts + +``` +artifacts.find({"module.build.name":"my-service","module.build.number":"42"}) + .include("name","type","sha1","md5") +``` + +### Find build dependencies + +``` +dependencies.find({"module.build.name":"my-service","module.build.number":"42"}) + .include("name","scope","type","sha1") +``` + +### Remote repository content + +Remote repo artifacts are stored in a `-cache` suffixed repo. Always query +the cache repo, not the remote repo itself: + +``` +items.find({"repo":"npm-remote-cache","name":{"$match":"*.tgz"}}) +``` + +## Gotchas + +- The request body is **plain text**, not JSON — use +`Content-Type: text/plain`. +- String values in criteria must be quoted, including numeric comparisons +(`"size":{"$gt":"1000"}` not `"size":{"$gt":1000}`). +- Remote repo content lives in `-cache`, not ``. +- Sort fields must appear in the result set (included explicitly or by +default). +- Non-admin `items` queries must return `repo`, `path`, `name`. +- Non-admin `builds` queries must return `name`, `number`, `repo`. +- Items connect to builds through checksum matching (SHA-1), so cross-domain +queries between items and builds are valid but traverse multiple joins. +- The `path` value for items at the **root** of a repository is `"."`, not +`""` or `"/"`. Use `"path":"."` to match root-level files. +- **Docker `list.manifest.json`** — multi-arch images store two manifest files per + tag: `manifest.json` (platform-specific manifest) and `list.manifest.json` (OCI + image index). Filtering by `"name":"manifest.json"` is correct for tag listing + (one result per tag), but silently excludes `list.manifest.json` entries. Use + `"name":{"$match":"*manifest.json"}` when querying by uploader, date range, or + any context where all manifest pushes should be counted. +- **`stat.downloads` filters do not match zero-download items** — never-downloaded + items lack a stats row so the join finds nothing. Use the client-side `jq` + approach in "Find never-downloaded files" above. +- `$match` uses SQL-style wildcards: `*` matches any characters, `?` matches +exactly one character. It is **not** regex. Literal `_` and `%` in patterns +are escaped automatically. +- The `builds.number` field is a **string**, not an integer. Build numbers +like `"42"`, `"1.0.3"`, and `"SNAPSHOT-1"` are all valid. +- Release bundle `type` values are uppercase strings: `"SOURCE"` or +`"TARGET"`. +- Dates accept both ISO 8601 format (`"2025-06-01T00:00:00.000Z"`) and +epoch milliseconds as a string (`"1719792000000"`). +- The server silently excludes trash, support-bundle, and in-transit +repository content from AQL results. If an item exists but doesn't appear +in results, it may be in one of these hidden repos. +- Virtual repo queries are rewritten to search the underlying physical repos. +The `repo` field in results shows the physical repo name, not the virtual +repo name you queried. + +## Official documentation + +- [Artifactory Query Language](https://docs.jfrog.com/artifactory/docs/artifactory-query-language) — overview and architecture +- [Query Structure and Syntax](https://docs.jfrog.com/artifactory/docs/aql-syntax) — domain queries, field references, JSON-like syntax rules +- [Search Criteria and Operators](https://docs.jfrog.com/artifactory/docs/aql-search-criteria) — comparators, wildcards, `$msp`, relative time +- [AQL Entities and Fields Reference](https://docs.jfrog.com/artifactory/docs/aql-entities-fields-reference) — complete field list for all domains +- [Query Output and Modifiers](https://docs.jfrog.com/artifactory/docs/aql-query-output) — `.include()`, `.sort()`, `.offset()`, `.limit()`, `.distinct()` +- [Query Execution and Permissions](https://docs.jfrog.com/artifactory/docs/aql-query-execution) — authentication, scoped tokens, HTTP errors, streaming +- [AQL Examples and Common Patterns](https://docs.jfrog.com/artifactory/docs/aql-examples) — ready-to-use queries by use case +- [Repository-Specific Queries](https://docs.jfrog.com/artifactory/docs/aql-repository-queries) — `.transitive()`, virtual repos, remote search +- [Performance and Operational Controls](https://docs.jfrog.com/artifactory/docs/aql-performance) — result limits, timeouts, rate limiting, optimization + diff --git a/plugin/skills/jfrog/references/artifactory-entities.md b/plugin/skills/jfrog/references/artifactory-entities.md new file mode 100644 index 0000000..1e4d950 --- /dev/null +++ b/plugin/skills/jfrog/references/artifactory-entities.md @@ -0,0 +1,236 @@ +# Artifactory entities + +When to read this file: + +- Working with **repositories** and you need to understand the difference between local, remote, virtual, and federated types. +- Managing **artifacts**, **properties**, or **package types**. +- Working with **builds**, **build promotion**, or **permission targets**. +- Debugging unexpected behavior related to repo types (e.g. upload failures, missing search results). + +For CLI commands see `artifactory-operations.md`. For API gaps see +`artifactory-api-gaps.md`. For AQL syntax see `artifactory-aql-syntax.md`. + +## Repositories + +A repository is the primary storage and resolution unit in Artifactory. Every +repo has a **key** (unique identifier), a **package type** (immutable after +creation), and a **repository class** (`rclass`) that determines its behavior. + +### Repository types + +| Type | `rclass` | Behavior | Stores artifacts? | +|------|----------|----------|-------------------| +| **Local** | `local` | Hosts artifacts deployed directly (upload, promote, copy, move) | Yes | +| **Remote** | `remote` | Proxies an external URL; downloads are cached in a companion `-cache` repo | Only in the `-cache` repo | +| **Virtual** | `virtual` | Aggregates multiple local and remote repos under a single URL for resolution | No (resolves from underlying repos) | +| **Federated** | `federated` | Local repo that bi-directionally synchronizes across Platform Deployments | Yes (replicated across sites) | + +### Key relationships and fields + +- `key` — unique repo identifier (e.g. `libs-release-local`) +- `packageType` — determines layout and protocol (see Package types below) +- `rclass` — `local`, `remote`, `virtual`, or `federated` +- `url` — (remote only) the external source URL being proxied +- `repositories` — (virtual only) ordered list of local/remote repos to aggregate +- `projectKey` — links repo to a JFrog Project (see `platform-access-entities.md`) +- `environments` — environments the repo is assigned to (used in RBAC and lifecycle) + +### System repositories + +Artifactory and Xray maintain several **system repositories** for internal +platform metadata. These are not user-created and should be excluded when +iterating over repositories for reporting, scanning, or auditing: + +| Pattern | Purpose | +|---------|---------| +| `release-bundles` | Release Bundles V1 metadata | +| `release-bundles-v2` | Release Bundles V2 metadata | +| `artifactory-build-info` | Default build info storage | +| `*-release-bundles` | Project-scoped Release Bundles V1 | +| `*-release-bundles-v2` | Project-scoped Release Bundles V2 | +| `*-build-info` | Project-scoped build info storage | +| `*-application-versions` | AppTrust application version metadata | + +Including these in aggregate queries (violation counts, storage reports, etc.) +produces misleading results because they contain platform metadata rather than +user artifacts. + +### Remote repository cache + +When Artifactory downloads an artifact through a remote repo, it stores the +cached copy in a **separate local repo** named `-cache`. This is +critical for: + +- **AQL queries** — search the `-cache` repo, not the remote repo key +- **Properties** — properties on cached artifacts live on the `-cache` repo +- **Storage calculations** — cached artifacts consume storage under the `-cache` repo + +The remote repo key itself is used for **configuration** (URL, credentials, +inclusion/exclusion patterns) but does not directly contain artifacts. + +### Virtual repository resolution + +A virtual repo aggregates **both local and remote repos** under a single URL. +It resolves artifacts by searching its underlying repos in the configured +**order** — when the same artifact exists in multiple underlying repos, the +first match wins. + +A virtual repo may designate one of its underlying **local** repos as the +**default deployment repository**. Uploads through the virtual URL are routed +to that local repo. Without a default deployment repo, the virtual repo is +read-only. + +```mermaid +erDiagram + VirtualRepo ||--o{ LocalRepo : "aggregates" + VirtualRepo ||--o{ RemoteRepo : "aggregates" + VirtualRepo ||--o| LocalRepo : "defaultDeploymentRepo" + RemoteRepo ||--|| CacheRepo : "has -cache" +``` + +## Artifacts + +An artifact is a file stored in a repository. Each artifact is uniquely +identified by the triple **repo + path + name**. + +Key attributes: +- `repo`, `path`, `name` — location identifier +- `size` — bytes +- `sha256`, `sha1`, `md5` — checksums (sha256 is the primary identifier for cross-referencing with builds and Xray) +- `created`, `modified`, `created_by`, `modified_by` — audit fields + +Artifacts are **content-addressable** — build info and Xray reference them by +checksum, not by path. Moving or copying an artifact changes its path but not +its checksum, so build associations follow the artifact. + +## Properties + +Key-value metadata pairs attached to artifacts or folders. + +- Keys are strings; values are strings or arrays of strings +- Set via `jf rt set-props`, queried via AQL or the properties API +- Commonly used for: build metadata, maturity labels, promotion tracking, cleanup policies +- Properties on remote-cached artifacts live on the `-cache` repo + +## Package types + +The `packageType` field on a repository determines how Artifactory interprets +its contents. It controls directory structure conventions, metadata extraction, +and which client protocols are supported (e.g. Docker registry API, npm +registry, Maven layout). + +Common types: `maven`, `gradle`, `npm`, `docker`, `pypi`, `nuget`, `go`, +`helm`, `rpm`, `debian`, `generic`. + +Package type is **immutable** — it cannot be changed after repo creation. Use +`generic` when no specific package type applies. + +## Build info + +A build info record captures CI/CD metadata: which artifacts were produced, +which dependencies were consumed, and the build environment. + +| Field | Description | +|-------|-------------| +| `name` + `number` | Unique identifier for a build run | +| `modules` | List of modules, each with its own artifacts and dependencies | +| `vcs` | Version control metadata (revision, URL, branch) | +| `buildAgent`, `agent` | CI tool info | +| `properties` | Custom build-level properties | + +Build info references artifacts **by checksum** (sha256). This means: +- A build can reference artifacts across multiple repositories +- Moving an artifact does not break the build association +- Xray scans build info by resolving checksums to components + +Lifecycle: collect → publish → (optionally) promote → (optionally) scan. + +## Build promotion + +Promotion changes a build's **status** and can copy or move its artifacts +from a source repo to a target repo. + +| Field | Description | +|-------|-------------| +| `status` | Target status label (e.g. `staged`, `released`) | +| `sourceRepo` | Where artifacts currently reside | +| `targetRepo` | Where artifacts should be moved/copied | +| `copy` | If `true`, copy instead of move | + +Promotion records are queryable via AQL (`build.promotions` domain) and the +build promotion API. + +## Permissions + +Permissions define RBAC policies mapping **resources** and **principals** +(users and groups) to **actions**. Two models exist: + +### Permissions V2 (Access Permissions) — current model + +Managed by the **Access service** (since Artifactory 7.72.0, recommended from +7.77.2). Supports all resource types. + +| Component | Description | +|-----------|-------------| +| `name` | Permission name | +| `resources` | Map of resource type → targets + actions | + +Resource types: `artifact` (repositories), `build`, `release_bundle`, +`destination` (Edge nodes), `pipeline_source`. + +Each resource contains: +- `targets` — map of target names/patterns to include/exclude patterns +- `actions.users` — map of username → list of actions +- `actions.groups` — map of group name → list of actions + +Actions use uppercase: `READ`, `ANNOTATE`, `DEPLOY/CACHE`, `DELETE/OVERWRITE`, +`MANAGE_XRAY_METADATA`, `MANAGE`. + +API: `POST/PUT/GET/DELETE /access/api/v2/permissions/{permissionName}`. + +Documentation: [Permissions](https://docs.jfrog.com/administration/docs/permissions). + +### Permission targets (V1) — legacy model + +Managed by **Artifactory**. Still functional and backwards compatible, but +V2 is recommended for new implementations. The CLI `jf rt permission-target-*` +commands use this API. + +| Component | Description | +|-----------|-------------| +| `repositories` | List of repo keys or patterns | +| `actions.users` | Map of username → list of actions | +| `actions.groups` | Map of group name → list of actions | + +Actions use lowercase: `read`, `write`, `annotate`, `delete`, `manage`. + +Does **not** support `destination` or `pipeline_source` resource types. + +API: `PUT /artifactory/api/security/permissions/{permissionName}`. + +### Key differences + +| Aspect | V1 (Permission Targets) | V2 (Access Permissions) | +|--------|------------------------|------------------------| +| Managed by | Artifactory | Access service | +| API base | `/artifactory/api/security/permissions/` | `/access/api/v2/permissions/` | +| Actions | lowercase (`read`, `write`) | uppercase (`READ`, `WRITE`) | +| Resource types | repos, builds, release bundles | + destinations, pipeline sources | +| Pattern fields | `includes_pattern` / `excludes_pattern` | `include_patterns` / `exclude_patterns` | +| CLI support | `jf rt permission-target-*` | No direct CLI commands (use REST) | + +For project-scoped RBAC, see Project roles in `platform-access-entities.md`. + +## Replication + +Replication synchronizes artifacts and properties between repositories, either +within the same instance or across Platform Deployments. + +| Type | Direction | Trigger | +|------|-----------|---------| +| **Push** | Source pushes to target | Scheduled or event-based | +| **Pull** | Target pulls from source | Scheduled | + +Replication configs are JSON templates applied per repository. Both artifact +content and properties are replicated. For federated repos, replication is +automatic and bi-directional across all member nodes. diff --git a/plugin/skills/jfrog/references/artifactory-operations.md b/plugin/skills/jfrog/references/artifactory-operations.md new file mode 100644 index 0000000..bab2b65 --- /dev/null +++ b/plugin/skills/jfrog/references/artifactory-operations.md @@ -0,0 +1,178 @@ +# Artifactory Operations + +CLI commands for managing Artifactory resources. All commands use the `jf rt` +namespace. Run `jf rt --help` to discover subcommands not listed here. + +## Repository management + +Repositories are created from JSON templates. The workflow is: + +1. Get a template: retrieve an existing repo config via + `jf api /artifactory/api/repositories/` + and modify it, or craft JSON manually. + Note: `jf rt repo-template` is interactive and cannot be used by agents. +2. Create: `jf rt repo-create ` +3. Update: `jf rt repo-update ` +4. Delete: `jf rt repo-delete --quiet` + +To list repositories, use: +`jf api /artifactory/api/repositories` + +## File operations + +- Upload: `jf rt upload ` +- Download: `jf rt download [target]` +- Search: `jf rt search ` +- Move: `jf rt move ` +- Copy: `jf rt copy ` +- Delete: `jf rt delete ` +- Set properties: `jf rt set-props "key=value"` +- Delete properties: `jf rt delete-props "key"` + +### Searching across repositories + +`jf rt search` expects a `/` argument. When the repo is unknown, +agents tend to use a leading wildcard (`jf rt search "*/path/..."`), which +generates an unscoped AQL internally and can time out on large instances. + +Use a direct AQL query with `name` and `path` criteria instead — omitting the +`repo` field searches all accessible repos via indexed columns: + +```bash +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" \ + -d 'items.find({ + "name":"", + "path":"" + }).include("repo","path","name","size","sha256")' +``` + +Add `"repo":""` to the criteria when the target repo is known, to +narrow the search further. + +## Build info + +**Project scoping rule:** Append `?project=` to **every** build detail +API call. When the user provides a project key, use it. When no project key +is provided, use `?project=default` (the built-in default project that covers +the `artifactory-build-info` repo). For AQL queries, scope by +`"repo":"-build-info"` (or `"repo":"artifactory-build-info"` for +the default project). + +**Server rule:** A 404 from a `?project=` build call is **not** a signal +to try a different server. Use only the resolved server; on any failure, +report and stop. See `SKILL.md` § *Server selection rules*. + +### Publishing builds + +- Collect env: `jf rt build-collect-env ` +- Add git info: `jf rt build-add-git ` +- Publish: `jf rt build-publish ` +- Promote: `jf rt build-promote ` +- Discard: `jf rt build-discard ` + +### Listing build names + +**Do not use `GET /api/build`** — it has no pagination and times out on large +instances. Always use AQL with `limit` and `offset`. + +**All builds** (no project scope): + +```bash +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" \ + -d 'builds.find().include("name","number","repo","created").sort({"$desc":["created"]}).offset(0).limit(100)' +``` + +**Project-scoped** — filter by the project's build-info repository +(`-build-info`, or `artifactory-build-info` for the default +project): + +```bash +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" \ + -d 'builds.find({"repo":"-build-info"}).include("name","number","repo","created").sort({"$desc":["created"]}).offset(0).limit(100)' +``` + +**Pagination:** The response includes a `range` object with `total` (total +matching records). If `total` exceeds the `limit`, tell the user: *"Showing +first 100 of N results (paginated). Ask for the next batch if needed."* +For subsequent pages, increment `offset` by 100. + +**Output rule (mandatory):** AQL returns one row per name+number pair. +Extract **unique build names** client-side (e.g. +`jq '[.[].builds.name] | unique'`). Present **only the deduplicated list of +build names** to the user. **Do not** include build numbers, timestamps, run +counts, or any per-run details in the response — not even as a "bonus" or +"most recent" table. The user is asking "what builds exist", not "what runs +happened". Only show run-level details if the user explicitly asks for them +in a follow-up. + +### Listing runs of a specific build + +```bash +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" \ + -d 'builds.find({"name":""}).include("name","number","repo","created").sort({"$desc":["created"]}).offset(0).limit(100)' +``` + +Add `"repo":"-build-info"` to the criteria when a project key +is known. Apply the same pagination rules as above. + +### Retrieving full build info + +Use the REST detail endpoint for a **single** build run. Always include +`?project=` (or `?project=default` when no key is provided): + +```bash +jf api "/artifactory/api/build//?project=" +``` + +This is the only `/api/build` endpoint that should be used — it returns a +single record and does not need pagination. + +### When a build is not found + +If the detail call returns 404, the build likely belongs to a different +project. **Ask the user for the project key** rather than searching across +repos or servers. + +### Repository listing vs build-info + +`GET /artifactory/api/repositories?project=&type=buildinfo` may return +an empty list even when project-scoped build info exists (for example under +a `*-build-info` repository). Prefer AQL to +discover builds; do not treat an empty repository +list as proof that no +builds exist. + +## Permissions + +Permission targets use JSON templates. +Note: `jf rt permission-target-template` is interactive. + +- Create: `jf rt permission-target-create ` +- Update: `jf rt permission-target-update ` +- Delete: `jf rt permission-target-delete ` + +## Users and groups + +- Create users: `jf rt users-create --csv ` +- Create single user: `jf rt user-create` (check `--help` for options) +- Delete users: `jf rt users-delete ` +- Create group: `jf rt group-create ` +- Delete group: `jf rt group-delete ` +- Add users to group: `jf rt group-add-users ` + +To get user details or update users, use `jf api`: +``` +jf api /access/api/v2/users/ +``` + +## Replication + +Replication configs use JSON templates. +Note: `jf rt replication-template` is interactive. + +- Create: `jf rt replication-create ` +- Delete: `jf rt replication-delete ` diff --git a/plugin/skills/jfrog/references/catalog-entities.md b/plugin/skills/jfrog/references/catalog-entities.md new file mode 100644 index 0000000..085bea0 --- /dev/null +++ b/plugin/skills/jfrog/references/catalog-entities.md @@ -0,0 +1,219 @@ +# Catalog entities + +When to read this file: + +- Querying **public package metadata** (descriptions, vulnerabilities, licenses, operational info). +- Working with the **Custom Catalog** (org-specific labels, package views, federation). +- Looking up **vulnerability details** beyond what Xray provides (advisories, EPSS, CWE, known exploits). +- Querying **OpenSSF scorecards**, **ML model metadata**, or **MCP service** registries. +- Using the OneModel GraphQL API with `publicPackages`, `customPackages`, + `publicSecurityInfo`, `publicLegalInfo`, `publicOperationalInfo`, + `publicCatalogLabels`, or `publicRemoteServices` query roots. + +Catalog entities are accessed via the **OneModel GraphQL API** +(`/onemodel/api/v1/graphql`). + +For the OneModel query workflow (credentials, schema fetch, validation, +execution), read `references/onemodel-graphql.md`. + +## Two catalog layers + +| Layer | Scope | Description | +|-------|-------|-------------| +| **Public Catalog** | Global | JFrog's curated package database — security, legal, and operational metadata for public packages across ecosystems | +| **Custom Catalog** | Organization | Org-specific overlay — custom labels, per-org package views, federation config | + +The Custom Catalog builds on top of the Public Catalog. A public package +can be enriched with org-specific labels and metadata through the Custom +Catalog without altering the underlying public data. + +## Public Catalog entities + +### PublicPackage + +A package as known to JFrog's global package database. + +| Field | Description | +|-------|-------------| +| `name` | Package name (e.g. `lodash`, `spring-boot-starter-web`) | +| `type` | Package type (e.g. `npm`, `maven`, `pypi`) | +| `ecosystem` | Ecosystem identifier | +| `description` | Rich-text description | +| `homepage`, `vcsUrl` | Package URLs | +| `vendor` | Maintainer or organization | +| `latestVersion` | Most recent version | +| `trendingScore` | Popularity score | +| `publishedAt`, `modifiedAt` | Timestamps | +| `mlModel` | ML model metadata (for HuggingFace etc.) | + +Connections: `versionsConnection`, `publicLabelsConnection`, `legalInfo`, +`operationalInfo`, `securityInfo`. + +Query: `publicPackages.searchPackages(where: {...})`. + +### PublicPackageVersion + +A specific version with security, legal, and operational analysis. + +| Field | Description | +|-------|-------------| +| `version` | Version string | +| `isLatest` | Whether this is the latest version | +| `isListedVersion` | Whether visible in Catalog UI | +| `publishedAt`, `modifiedAt` | Timestamps | +| `trendingScore` | Version-level popularity | +| `dependencies` | Dependency information | +| `mlModelMetadata`, `mlInfo` | ML/AI-related metadata | + +Each version carries three info blocks: +- `securityInfo` — vulnerability data, maliciousness, contextual analysis +- `legalInfo` — licenses, copyrights +- `operationalInfo` — end-of-life, OpenSSF scores, popularity metrics + +### PublicVulnerability + +Vulnerability data richer than what Xray violations expose. Useful for +deep-dive security analysis and advisory lookups. + +| Field | Description | +|-------|-------------| +| `name` | CVE identifier (e.g. `CVE-2021-44228`) | +| `ecosystem` | Affected ecosystem | +| `severity` | `CRITICAL`, `HIGH`, `MEDIUM`, `LOW` | +| `description` | Detailed impact description | +| `cvss` | CVSS scores — v2, v3, **and v4** | +| `epss` | EPSS (Exploit Prediction Scoring System) — exploit likelihood | +| `knownExploit` | Known exploit information | +| `withdrawn` | Whether the CVE has been retracted | +| `aliases` | Alternative identifiers | +| `references` | Advisory URLs | +| `publishedAt`, `modifiedAt` | Timestamps | + +Advisory sources (via `advisories` connection): +- **NVD** — NIST National Vulnerability Database +- **GHSA** — GitHub Security Advisory +- **JFrog Advisory** — JFrog's own research (includes impact reasons) +- **Debian Security Tracker** +- **RedHat OVAL** + +Additional connections: `cwesConnection` (CWE entries), `cpesConnection` +(CPE entries), `publicPackageInfo` (affected packages and versions). + +Query: `publicSecurityInfo.searchVulnerabilities(where: {...})`. + +#### Filtering limitations + +`searchVulnerabilities` can filter by CVE name, ecosystem, severity, CVSS, +EPSS, known exploit status, and publication date — but **not** by affected +package name. There is no `hasPublicPackageInfoWith` or similar filter on +`PublicVulnerabilityWhereInput`. To find vulnerabilities affecting a specific +package, use one of these alternatives: + +- **Version-level security info** (GraphQL): query + `publicPackages.getPackage(type, name)` and navigate to + `versionsConnection → securityInfo → vulnerabilitiesConnection` to get + CVEs affecting specific versions. +- **Individual CVE lookup**: use `searchVulnerabilities(where: { name: "" })` + and inspect `publicPackageInfo.vulnerablePublicPackagesConnection` on the + `generic` ecosystem entry. + +#### Ecosystem multiplicity + +A single CVE appears as multiple `PublicVulnerability` entries — one per +ecosystem. The `ecosystem` field determines which entry you see: + +| Ecosystem | Contains | +|-----------|----------| +| `generic` | Non-OS package-level data (npm, maven, pypi, go, etc.) — includes `publicPackageInfo` with vulnerable versions and fix versions | +| `debian`, `redhat`, `ubuntu`, etc. | OS-specific advisory data — severity may differ from NVD; `publicPackageInfo` is typically empty (OS packages are tracked separately) | + +When looking up a CVE by name, `searchVulnerabilities(where: { name: "" })` +returns all ecosystem entries. To get affected packages and fix versions for +libraries like npm or maven, filter for or focus on the `generic` ecosystem +entry. `getVulnerability` requires both `name` and `ecosystem` — use +`searchVulnerabilities` when the ecosystem is unknown. + +### PublicLicense + +License metadata with permission, condition, and limitation details. + +| Field | Description | +|-------|-------------| +| `name` | License name (e.g. `Apache-2.0`, `MIT`) | +| `spdxId` | SPDX identifier | +| `permissions` | What the license permits | +| `limitations` | Restrictions imposed | +| `patentConditions` | Patent grant conditions | +| `noticeFiles` | Required notices | + +Query: `publicLegalInfo.searchLicenses(where: {...})`. + +### PublicPackageOperationalInfo + +Operational risk assessment for packages and versions. + +| Entity | Key data | +|--------|----------| +| **OpenSSF scorecard** | Overall score, individual checks with scores and pass/fail | +| **End-of-life** | Whether the package or version is EOL, justification | +| **Popularity** | JFrog popularity by segment and subscription tier, download counts | + +### MCP services and tools + +The Public Catalog also indexes MCP (Model Context Protocol) services: + +| Entity | Description | +|--------|-------------| +| `PublicMcpService` | An MCP service with name, description, version | +| `PublicMcpTool` | A tool exposed by an MCP service with arguments | +| `PublicMcpRemote` | Remote MCP server configuration | + +Query: `publicRemoteServices.searchMcpServices(where: {...})`. + +## Custom Catalog entities + +### CustomPackage + +A package in the organization's private catalog view. + +| Field | Description | +|-------|-------------| +| `customCatalogId` | Org-scoped identifier | +| `name`, `type`, `ecosystem`, `namespace` | Package identity | +| `isListedPackage` | Whether visible in Catalog UI | +| `customCatalogAddedAt`, `customCatalogModifiedAt` | Org-specific timestamps | + +Connections: `versionsConnection`, `legalInfo`, +`customCatalogLabelsConnection`. + +### CustomCatalogLabel + +Organization-defined labels for categorizing packages. + +| Field | Description | +|-------|-------------| +| `name` | Label name | +| `description` | What the label represents | +| `color` | Display color | +| `labelType` | `MANUAL` or `AUTOMATIC` | +| `assignmentInfo` | How and when the label was assigned | + +Labels can be assigned to both custom packages and public packages/versions +within the org's catalog scope. The Custom Catalog mutations allow +creating, updating, and deleting labels. + +### CustomCatalogFederation + +Configuration for federating catalog data across JFrog deployments. + +## Catalog vs. Xray vs. Stored Packages + +These three domains provide different views of package and security data: + +| Aspect | Catalog | Xray | Stored Packages | +|--------|---------|------|-----------------| +| **Scope** | Global knowledge base + org overlay | Instance-scoped scanning | Instance-scoped storage | +| **Security** | CVE advisories, EPSS, CVSS v2/v3/v4, known exploits | Watches, policies, violations | Vulnerability summary (deprecated) | +| **Packages** | Public metadata (description, homepage, OpenSSF) | Components identified during scanning | Packages/versions stored in Artifactory | +| **Access** | GraphQL only | REST + CLI (`jf api /xray/...`) | GraphQL only | +| **Use case** | Research, compliance reporting, package evaluation | Runtime enforcement, CI/CD gating | Inventory, location queries | diff --git a/plugin/skills/jfrog/references/general-bulk-operations-and-agent-patterns.md b/plugin/skills/jfrog/references/general-bulk-operations-and-agent-patterns.md new file mode 100644 index 0000000..2b44979 --- /dev/null +++ b/plugin/skills/jfrog/references/general-bulk-operations-and-agent-patterns.md @@ -0,0 +1,93 @@ +# Bulk operations and agent execution patterns + +Platform-wide guidance for agents that gather data from multiple JFrog products +(Artifactory, Xray, Access, Distribution, etc.), run long shell +sequences, or parallelize work. Product-specific field names and endpoints live +in the other `references/*` files; this document describes **patterns**, not +one workflow. + +## List vs detail responses + +Many REST surfaces expose a **light list** (keys, names, minimal fields) and a +**richer GET by id or key**. Fields needed for audits, reporting, joins, or +permission checks may appear **only** on the detail response. Before building a +multi-step flow on a single list call, confirm in API docs or with a sample GET +whether the fields you need are present. + +## Volume, batching, and timeouts + +- Estimate **N** round-trips (list + per-item GETs, paginated APIs, etc.) before + starting so execution time and tool timeouts stay predictable. +- Prefer batching independent reads in one Shell invocation when credentials and + tier match (see SKILL.md **Batch and parallel execution**). +- Split very large work across chunks, parallel Shell calls, or subagents when + the skill's tiering guidance says so. +- Before starting an N+1 loop (list + per-item detail), **estimate wall time** + as roughly `N * 1.5s` for sequential calls. Set `block_until_ms` to at + least that estimate plus a 30-second buffer. +- For loops exceeding ~60 items, prefer a single Shell invocation that writes + progress to a log file (`>> /tmp/jf-progress-$$.log`) so partial results + are visible even if the job is interrupted. +- If the task is read-only and items are independent, consider Tier 2 or + Tier 3 parallelism (see `general-parallel-execution.md`) to reduce total time — + but respect rate limits and keep concurrency modest (4-8 parallel calls). + +## Parallelism and shared files + +**Unsafe:** Multiple concurrent processes appending lines to the **same** file +(JSONL, logs, ndjson) without synchronization. Output can interleave on one +line and break parsers (e.g. JSON "Extra data" errors). + +**Safer:** + +- Write sequentially to one file; or +- One temp file per worker or chunk, then concatenate; or +- Use advisory locking (`flock`) if one file must be shared. + +For bulk API or CLI output files, use `/tmp` or `mktemp`; do not use +`~/.jfrog/skills-cache/` except for `jfrog-skill-state.json` and the OneModel +schema file (see main SKILL.md). + +## Shell hygiene + +- Use `set -euo pipefail` in non-trivial scripts so failures are not silent. +- Use unique temp paths (e.g. `$$` in the filename) and **echo the expanded + path** so it can be reused across Shell calls (see SKILL.md **Preserving + command output** for the `$$` + echo, session ID, and hardcoded patterns). +- Parse CLI and API JSON with **`jq`**. + +## Safe multi-response collection + +When looping over items (repos, builds, users) and fetching detail for each: + +1. Save each response to a variable or per-item file. +2. Validate with `jq -e . >/dev/null 2>&1` before appending. +3. On validation failure, write a structured error line so the caller can + report partial results instead of crashing. +4. After the loop, `jq -s '.' results.ndjson` to produce a single array. + +```bash +: >results.ndjson +while read -r key; do + body=$(jf api "/artifactory/api/repositories/$key" || true) + if echo "$body" | jq -e . >/dev/null 2>&1; then + echo "$body" | jq -c . >>results.ndjson + else + printf '{"key":"%s","_error":"invalid_response"}\n' "$key" >>results.ndjson + fi +done < <(jq -r '.[].key' list.json) +jq -s '.' results.ndjson > details.json +``` + +Never pipe a loop of `jf api` calls directly into `jq -s` without +per-body validation. + +## Where to find product specifics + +- Artifactory REST nuances: `references/artifactory-api-gaps.md` +- Platform admin / Access: `references/platform-admin-api-gaps.md` +- JFrog Projects (endpoints): `references/projects-api.md` +- Joining Artifactory repos to Projects (`projectKey`, roles, environments): + `references/platform-access-entities.md` +- Platform API invocation (all products through `jf api`): see + `SKILL.md` § *Invoking platform APIs with `jf api`* diff --git a/plugin/skills/jfrog/references/general-parallel-execution.md b/plugin/skills/jfrog/references/general-parallel-execution.md new file mode 100644 index 0000000..a0bb9ba --- /dev/null +++ b/plugin/skills/jfrog/references/general-parallel-execution.md @@ -0,0 +1,131 @@ +# Batch and Parallel Execution + +When a task requires multiple independent operations, use the lightest +parallelism mechanism that fits. Three tiers are available, from lightest to +heaviest: + +| Tier | Mechanism | Best for | +|------|-----------|----------| +| 1 | Single Shell call with `&&` | Few commands, same credentials | +| 2 | Parallel Shell tool calls | Independent commands that can run concurrently | +| 3 | Parallel subagents (Task tool) | Large multi-step jobs where each branch needs its own reasoning | + +## Tier 1: Batch within a single Shell call + +Combine independent commands with `&&`. All JFrog API calls go through the +same `jf api` command and the same `jf config` server, so batching them +together is both safe and efficient: + +```bash +jf api /artifactory/api/repositories > /tmp/jf-repos-$$.json && \ +jf api /artifactory/api/system/ping > /tmp/jf-ping-$$.json && \ +jf api /artifactory/api/storageinfo > /tmp/jf-storage-$$.json +``` + +Cross-product reads batch the same way: + +```bash +jf api /access/api/v2/users/ > /tmp/jf-users-$$.json && \ +jf api /access/api/v2/groups/ > /tmp/jf-groups-$$.json && \ +jf api /access/api/v2/permissions/ > /tmp/jf-perms-$$.json +``` + +## Tier 2: Parallel Shell tool calls + +Use multiple Shell tool calls in the same message when the commands are +independent and the total runtime benefits from concurrency: + +```bash +# Shell call 1 — echo the expanded path so the agent can reference it later +OUT=/tmp/jf-repos-$$.json +jf api /artifactory/api/repositories > "$OUT" && echo "$OUT" + +# Shell call 2 (parallel) — same pattern, different PID +OUT=/tmp/jf-users-$$.json +jf api /access/api/v2/users/ > "$OUT" && echo "$OUT" +``` + +Each parallel Shell call gets a different PID, so `$$` expands to different +values. Echo the path so the agent knows the literal filename for cross-call +use (see SKILL.md **Preserving command output**). + +## Tier 3: Parallel subagents + +For tasks with multiple independent branches that each require several steps +or their own reasoning — such as generating a platform health report with +separate sections, auditing both repository config and security policies, or +comparing configurations across servers the user explicitly named — launch +parallel subagents using the Task tool. + +Each subagent runs autonomously, executes its own CLI/API calls, and returns +a structured result. The parent agent assembles the final answer. + +### Example — platform audit with three parallel subagents + +``` +Subagent 1 (shell): "Collect repository data" + → jf api /artifactory/api/repositories + → jf api /artifactory/api/storageinfo + → Return repo count, types, total size + +Subagent 2 (shell): "Collect security configuration" + → jf api /xray/api/v2/policies + → jf api /xray/api/v2/watches + → Return policy count, watch count, coverage gaps + +Subagent 3 (shell): "Collect user and permission data" + → jf api /access/api/v2/users/ + → jf api /access/api/v2/groups/ + → jf api /access/api/v2/permissions/ + → Return user count, group count, admin users +``` + +All three subagents run concurrently. Once all complete, the parent agent +merges their results into a unified report. + +### How to structure a subagent prompt + +1. State the goal clearly (e.g. "Collect all Xray policies and watches"). +2. Provide the exact commands to run, or name the API tier and let the + subagent discover via `--help`. +3. Tell the subagent to save output to `/tmp/jf-