

An ICARAX Tech Blog implementation guide
⚠️ Disclaimer – This guide is purely defensive. It shows how to detect, verify, and harden your Artifactory installation against the publicly disclosed vulnerability. No exploit code is included.
| Item | Why you need it | Recommended version |
|---|---|---|
| Access to a JFrog Artifactory instance (self‑hosted or SaaS) | To query the REST API and apply hardening steps | Any version – you’ll verify the exact build |
Administrative API key or username/password with System Admin rights | Required for /api/system/* and security endpoints | – |
| Python 3.9+ (or Node.js 18+) | Runtime for the sample scripts | – |
| Git (optional) | To clone the example repo if you prefer | – |
| curl (for quick manual checks) | Helpful for debugging | – |
| Network access – outbound HTTPS to your Artifactory URL | API calls | – |
Tip: If you are using Artifactory Cloud, generate an API key from User Profile → Edit Profile → API Key.
# 1️⃣ Create a virtual environment (recommended)
python -m venv venv
source venv/bin/activate # on Windows: venv\Scripts\activate
# 2️⃣ Install the only dependency we need – requests (with urllib3 security extras)
pip install --upgrade requests
pip install python-dotenv # for loading .env files (optional but handy)
# 1️⃣ Initialise a new Node project
npm init -y
# 2️⃣ Install dependencies
npm install axios dotenv # axios for HTTP, dotenv for env vars
# If you prefer TypeScript:
npm install --save-dev typescript @types/node ts-node
npx tsc --init # creates a basic tsconfig.json
Note: The code snippets below work with plain JavaScript; the TypeScript version is identical aside from type annotations.
The goal of the samples is to:
All samples share the same environment‑variable driven configuration (see Step 4).
artifactory_hardening.py)#!/usr/bin/env python3
"""
ICARAX Blog: Defensive script for CVE‑2024‑XXXXX (JFrog Artifactory).
Features:
- Retrieve Artifactory version
- Check anonymous access flag
- List local repositories
- (Optional) Trigger an Xray scan on a repo
"""
import os
import sys
import json
import logging
from typing import Any, Dict, List
import requests
from requests.auth import HTTPBasicAuth
from dotenv import load_dotenv
# ----------------------------------------------------------------------
# Load environment variables from .env (if present)
# ----------------------------------------------------------------------
load_dotenv() # expects ARTIFACTORY_URL, ARTIFACTORY_USER, ARTIFACTORY_TOKEN
# ----------------------------------------------------------------------
# Configuration (read from env, with sensible defaults where possible)
# ----------------------------------------------------------------------
ARTIFACTORY_URL = os.getenv("ARTIFACTORY_URL", "").rstrip("/")
ARTIFACTORY_USER = os.getenv("ARTIFACTORY_USER")
ARTIFACTORY_TOKEN = os.getenv("ARTIFACTORY_TOKEN") # API key or password
VERIFY_SSL = os.getenv("ARTIFACTORY_VERIFY_SSL", "true").lower() == "true"
TIMEOUT = int(os.getenv("ARTIFACTORY_TIMEOUT", "15"))
if not ARTIFACTORY_URL or not (ARTIFACTORY_USER and ARTIFACTORY_TOKEN):
sys.exit(
"❌ Missing required environment variables: "
"ARTIFACTORY_URL, ARTIFACTORY_USER, ARTIFACTORY_TOKEN"
)
# ----------------------------------------------------------------------
# Logging setup
# ----------------------------------------------------------------------
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s - %(message)s",
)
log = logging.getLogger("artifactory_hardening")
# ----------------------------------------------------------------------
# Helper: generic GET request with error handling
# ----------------------------------------------------------------------
def _get(endpoint: str, params: Dict[str, Any] | None = None) -> Dict[str, Any]:
url = f"{ARTIFACTORY_URL}{endpoint}"
auth = HTTPBasicAuth(ARTIFACTORY_USER, ARTIFACTORY_TOKEN)
try:
resp = requests.get(
url,
auth=auth,
params=params,
timeout=TIMEOUT,
verify=VERIFY_SSL,
)
resp.raise_for_status()
return resp.json()
except requests.RequestException as exc:
log.error("GET %s failed: %s", endpoint, exc)
raise
# ----------------------------------------------------------------------
# 1️⃣ Fetch Artifactory version
# ----------------------------------------------------------------------
def get_version() -> str:
data = _get("/api/system/version")
version_str = data.get("version", "unknown")
log.info("Artifactory version: %s", version_str)
return version_str
# ----------------------------------------------------------------------
# 2️⃣ Check anonymous access status
# ----------------------------------------------------------------------
def get_anonymous_access() -> bool:
"""
Returns True if anonymous access is enabled.
Endpoint: /api/security/anonymouse (note spelling!)
"""
data = _get("/api/security/anonymouse")
enabled = data.get("enabled", False)
log.info("Anonymous access enabled: %s", enabled)
return enabled
# ----------------------------------------------------------------------
# 3️⃣ List local repositories (requires at least read permission)
# ----------------------------------------------------------------------
def list_local_repos() -> List[str]:
data = _get("/api/repositories?type=local")
repos = [r.get("key") for r in data if isinstance(r, dict) and r.get("key")]
log.info("Found %d local repositories", len(repos))
for r in repos:
log.debug(" - %s", r)
return repos
# ----------------------------------------------------------------------
# 4️⃣ (Optional) Trigger an Xray scan on a repo
# ----------------------------------------------------------------------
def trigger_xray_scan(repo_key: str) -> None:
"""
Requires JFrog Xray to be installed and linked.
Endpoint: POST /api/v1/scan (Xray) – note the different base path.
"""
# Xray uses the same base URL but a different context path
xray_url = f"{ARTIFACTORY_URL}/xray"
scan_endpoint = f"{xray_url}/api/v1/scan"
payload = {
"repo": repo_key,
"scan_type": "repository",
}
auth = HTTPBasicAuth(ARTIFACTORY_USER, ARTIFACTORY_TOKEN)
try:
resp = requests.post(
scan_endpoint,
json=payload,
auth=auth,
timeout=TIMEOUT,
verify=VERIFY_SSL,
)
resp.raise_for_status()
log.info("Xray scan triggered for repo %s: %s", repo_key, resp.json())
except requests.RequestException as exc:
log.error("Failed to trigger Xray scan for %s: %s", repo_key, exc)
raise
# ----------------------------------------------------------------------
# Main orchestration
# ----------------------------------------------------------------------
def main() -> None:
try:
version = get_version()
# Example: you could compare against a known vulnerable range here
# if version < "7.XX.YY": log.warning("Potentially vulnerable!")
anon_enabled = get_anonymous_access()
if anon_enabled:
log.warning(
"⚠️ Anonymous access is ENABLED – consider disabling it immediately."
)
repos = list_local_repos()
# Example: scan the first repo if Xray is available
if repos and os.getenv("ENABLE_XRAY_SCAN", "false").lower() == "true":
trigger_xray_scan(repos[0])
except Exception as exc: # pragma: no cover – defensive top-level catch
log.exception("Unhandled error: %s", exc)
sys.exit(1)
if __name__ == "__main__":
main()
How to run
# Create a .env file (see Step 4) or export variables directly
export ARTIFACTORY_URL="https://artifactory.example.com"
export ARTIFACTORY_USER="admin"
export ARTIFACTORY_TOKEN="YOUR_API_KEY"
# Optional: enable Xray scan demo
export ENABLE_XRAY_SCAN="true"
python artifactory_hardening.py
artifactoryHardening.ts)/**
* ICARAX Blog: Defensive Node.js/TS script for CVE‑2024‑XXXXX (JFrog Artifactory).
*
* Demonstrates:
* - Version check
* - Anonymous access flag
* - List local repos
* - Optional Xray scan trigger
*
* Requires Node.js ≥18 and the packages installed in Step 2.
*/
import axios, { AxiosInstance } from "axios";
import dotenv from "dotenv";
// ----------------------------------------------------------------------
// Load .env (if present)
// ----------------------------------------------------------------------
dotenv.config();
// ----------------------------------------------------------------------
// Configuration
// ----------------------------------------------------------------------
const ARTIFACTORY_URL = process.env.ARTIFACTORY_URL?.replace(/\/+$/, "");
const ARTIFACTORY_USER = process.env.ARTIFACTORY_USER;
const ARTIFACTORY_TOKEN = process.env.ARTIFACTORY_TOKEN;
const VERIFY_SSL = process.env.ARTIFACTORY_VERIFY_SSL?.toLowerCase() !== "false";
const TIMEOUT = Number(process.env.ARTIFACTORY_TIMEOUT) || 15;
if (!ARTIFACTORY_URL || !ARTIFACTORY_USER || !ARTIFACTORY_TOKEN) {
console.error(
"❌ Missing required env vars: ARTIFACTORY_URL, ARTIFACTORY_USER, ARTIFACTORY_TOKEN"
);
process.exit(1);
}
// ----------------------------------------------------------------------
// Create an axios instance with basic auth
// ----------------------------------------------------------------------
const api: AxiosInstance = axios.create({
baseURL: ARTIFACTORY_URL,
timeout: TIMEOUT,
httpsAgent: undefined, // let axios handle SSL; Node will respect NODE_TLS_REJECT_UNAUTHORIZED
validateStatus: (status) => status < 500, // we treat 4xx as resolvable errors
});
// Apply basic auth to every request
api.interceptors.request.use((config) => {
config.auth = {
username: ARTIFACTORY_USER,
password: ARTIFACTORY_TOKEN,
};
return config;
});
// ----------------------------------------------------------------------
// Helper wrapper for consistent error handling
// ----------------------------------------------------------------------
async function safeGet<T>(url: string): Promise<T> {
try {
const { data } = await api.get<T>(url, { validateStatus: (s) => s < 400 });
if (data === undefined) throw new Error("Empty response");
return data;
} catch (err: any) {
if (err.response) {
console.error(
`❌ GET ${url} failed – ${err.response.status}: ${err.response.data}`
);
} else {
console.error(`❌ GET ${url} failed – ${err.message}`);
}
throw err;
}
}
// ----------------------------------------------------------------------
// 1️⃣ Get Artifactory version
// ----------------------------------------------------------------------
async function getVersion(): Promise<string> {
const resp = await safeGet<{ version: string }>("/api/system/version");
console.log(`🔖 Artifactory version: ${resp.version}`);
return resp.version;
}
// ----------------------------------------------------------------------
// 2️⃣ Check anonymous access
// ----------------------------------------------------------------------
async function getAnonymousAccess(): Promise<boolean> {
// Note the endpoint spelling: anonymouse (not anonymous)
const resp = await safeGet<{ enabled: boolean }>("/api/security/anonymouse");
console.log(`🕵️♂️ Anonymous access enabled: ${resp.enabled}`);
return resp.enabled;
}
// ----------------------------------------------------------------------
// 3️⃣ List local repositories
// ----------------------------------------------------------------------
async function listLocalRepos(): Promise<string[]> {
const resp = await safeGet<any[]>("/api/repositories?type=local");
const keys = resp
.filter((r): r is { key: string } => typeof r?.key === "string")
.map((r) => r.key);
console.log(`📦 Found ${keys.length} local repositories`);
keys.forEach((k) => console.log(` - ${k}`));
return keys;
}
// ----------------------------------------------------------------------
// 4️⃣ (Optional) Trigger Xray scan on a repo
// ----------------------------------------------------------------------
async function triggerXrayScan(repoKey: string): Promise<void> {
const xrayBase = `${ARTIFACTORY_URL}/xray`;
const xrayApi = axios.create({
baseURL: xrayBase,
timeout: TIMEOUT,
httpsAgent: undefined,
});
xrayApi.interceptors.request.use((cfg) => {
cfg.auth = { username: ARTIFACTORY_USER, password: ARTIFACTORY_TOKEN };
return cfg;
});
try {
const { data } = await xrayApi.post(
"/api/v1/scan",
{ repo: repoKey, scan_type: "repository" },
{ validateStatus: (s) => s < 400 }
);
console.log(`🚀 Xray scan triggered for ${repoKey}:`, data);
} catch (err: any) {
if (err.response) {
console.error(
`❌ Xray scan failed – ${err.response.status}: ${err.response.data}`
);
} else {
console.error(`❌ Xray scan failed – ${err.message}`);
}
throw err;
}
}
// ----------------------------------------------------------------------
// Main driver
// ----------------------------------------------------------------------
(async () => {
try {
const version = await getVersion();
// Example vulnerability check – replace with actual vulnerable range if known
// const isVulnerable = semver.lt(version, "7.XX.YY");
// if (isVulnerable) console.warn("⚠️ Version may be vulnerable!");
const anon = await getAnonymousAccess();
if (anon) {
console.warn(
"⚠️ Anonymous access is ENABLED – disable it immediately via UI or API."
);
}
const repos = await listLocalRepos();
if (
repos.length > 0 &&
process.env.ENABLE_XRAY_SCAN?.toLowerCase() === "true"
) {
await triggerXrayScan(repos[0]);
}
} catch (e) {
console.error("💥 Unexpected error:", e);
process.exit(1);
}
})();
How to run (TS)
# If you saved as .ts
npx ts-node artifactoryHardening.ts
# Or compile first
npx tsc
node dist/artifactoryHardening.js
How to run (plain JS) – rename the file to .js and drop the type annotations; the logic stays identical.
Create a .env file in the project root (or export the variables in your shell).
Never commit this file to source control; add it to .gitignore.
# .env – Example (replace with your own values)
ARTIFACTORY_URL=https://artifactory.example.com
ARTIFACTORY_USER=admin
ARTIFACTORY_TOKEN=YOUR_API_KEY_OR_PASSWORD
# Optional toggles
ARTIFACTORY_VERIFY_SSL=true # set false only for self‑signed certs in dev
ARTIFACTORY_TIMEOUT=20 # seconds
ENABLE_XRAY_SCAN=false # set true to demo an Xray scan trigger
Explanation of each variable
| Variable | Purpose | Typical value |
|---|---|---|
ARTIFACTORY_URL | Base URL of your Artifactory instance (no trailing slash) | https://artifactory.mycorp.com |
ARTIFACTORY_USER | Username with System Admin (or at least Read on /api/system/* and /api/security/*) | admin or a service account |
ARTIFACTORY_TOKEN | API key or password for the user above | AKCp8j… |
ARTIFACTORY_VERIFY_SSL | Whether to validate TLS certificates | true (production) |
ARTIFACTORY_TIMEOUT | HTTP timeout in seconds | 15 |
ENABLE_XRAY_SCAN | If set to true, the script will attempt to launch an Xray scan on the first local repo (requires Xray linked) | false |
Tip: For CI/CD pipelines, inject these as secrets (GitHub Actions, GitLab CI, Azure Pipelines, etc.) rather than a file.
| Pattern | When to use it | Code snippet (Python) | Code snippet (JS/TS) |
|---|---|---|---|
| Retry with exponential backoff | Transient network issues or rate‑limits | See requests.adapters.HTTPAdapter(max_retries=Retry(...)) | Use axios-retry library |
| Pagination handling | When listing many artifacts/repos (Artifactory caps at 1000 per page) | Loop with ?offset= & limit= parameters | Same – keep calling until X-RateLimit-Remaining hits 0 |
| Caching version info | Avoid hitting /api/system/version on every run | Store in a local file with TTL (e.g., cachetools) | Use node-cache or simple fs.writeFileSync + timestamp |
| Structured logging | Centralised log aggregation (ELK, Splunk) | logging.Logger with JSON formatter (python-json-logger) | pino or winston with JSON transport |
| Feature flag for unsafe ops | Enable/disable Xray scan or other heavy calls via env | if os.getenv("ENABLE_XRAY_SCAN") == "true": … | Same with process.env.ENABLE_XRAY_SCAN |
| Health‑check endpoint | Expose a tiny HTTP endpoint that runs the version check | Use FastAPI or Flask to expose /health | Use Express route /health |
| Symptom | Likely cause | Fix |
|---|---|---|
401 Unauthorized | Wrong credentials or missing API key | Verify ARTIFACTORY_USER/ARTIFACTORY_TOKEN. Ensure the token is not expired and has System or Read scope. |
403 Forbidden | User lacks required permissions (e.g., trying to call /api/security/anonymouse without admin) | Grant the user the System Admin role or at least Security > Manage permission. |
SSL: CERTIFICATE_VERIFY_FAILED | Self‑signed or internal CA not trusted | Set ARTIFACTORY_VERIFY_SSL=false only for testing, or add your CA to the system trust store (REQUESTS_CA_BUNDLE for Python, NODE_EXTRA_CA_CERTS for Node). |
429 Too Many Requests | Hitting Artifactory rate limit | Implement retry‑after header handling; add a back‑off strategy. |
| Empty repository list | Token only has read on specific repos, not global | Either broaden token scope or call /api/repositories?type=local&reponame=<specific> for each repo you know you have access to. |
Xray scan call returns 404 | Xray not installed or not linked to this Artifactory instance | Verify Xray is installed (/api/xray/system/ping) and that the instance is paired. |
| Script hangs / timeout | Network firewall blocking outbound port 443 | Test with curl -v https://<your‑artifactory>/api/system/version. Open required outbound HTTPS. |
Logs show None for version | Response not JSON (maybe HTML error page) | Check raw response (resp.text) – often a login redirect due to missing auth. Ensure auth header is being applied. |
Debug tip:
Add print(resp.request.headers) (Python) or console.log(config.headers) (JS) right before the request to confirm the Authorization: Basic <base64> header is present.
Before you push any of these scripts into CI/CD, automation, or a monitoring tool, run through this checklist:
| ✅ Item | Why it matters |
|---|---|
| Least‑privilege credentials | Create a dedicated service account with only the permissions needed (/api/system/version, /api/security/anonymouse, /api/repositories). Avoid using your personal admin token. |
| Secure secret storage | Use a vault (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault) or CI secret store; never hard‑code tokens. |
| TLS verification enabled | In production, keep ARTIFACTORY_VERIFY_SSL=true. Only disable for isolated dev environments with a documented risk acceptance. |
| Idempotent runs | The scripts should be safe to run repeatedly (they only read data; the optional Xray scan is guarded by a flag). |
| Logging & audit | Ensure logs are sent to a central system and that they do not contain secrets (mask tokens). |
| Version baseline | Record the known-good version of your Artifactory deployment. Alert if the version drops (indicating a possible rollback or tampering). |
| Alert on anonymous access | If get_anonymous_access() returns true, trigger an immediate alert (PagerDuty, Slack, email) and create a ticket to disable it. |
| Xray integration check | If you rely on Xray scanning, verify the pairing status (/api/xray/system/ping) before attempting a scan. |
| Rate‑limit awareness | Respect Retry-After headers; configure a back‑off (e.g., 1s → 2s → 4s) to avoid being blocked. |
| Dependency hygiene | Keep requests, axios, dotenv, etc., up‑to‑date (npm audit, pip check). |
| Test in a staging clone | Run the scripts against a non‑production Artifactory clone first to confirm they behave as expected. |
| Document the runbook | Add a short README in your repo that explains: What the script does, which env vars are needed, how to interpret the output, and who to contact on failure. |
| Post‑run validation | After disabling anonymous access or applying a patch, re‑run the script to confirm the change took effect. |
| Backup before config changes | If you ever decide to toggle features via the API (e.g., disable anonymous access), snapshot the current security configuration first (/api/system/configuration). |
You now have:
artifactory_hardening.py)artifactoryHardening.ts)Use these building blocks to:
Stay safe, keep your artifacts signed, and remember: the best defense is continuous verification paired with least‑privilege access.
Happy hardening! 🚀
(If you found this useful, consider starring the repo or sharing it with your DevSecOps team.)
Source: Security Week AI
Follow ICARAX for more AI insights and tutorials.
