Files
Miasma_Archive/src/mutator/jfrognpm/auth.ts
T
2026-06-13 09:36:13 -04:00

216 lines
6.2 KiB
TypeScript

// JFrog Artifactory credential validation.
// Supports API keys (X-JFrog-Art-Api), Bearer tokens (JWT + reftkn), and Basic auth.
import { logUtil } from "../../utils/logger";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export type JfrogCredentialType = "api-key" | "bearer" | "basic";
export interface JfrogCredential {
type: JfrogCredentialType;
value: string;
}
export interface JfrogSession {
baseUrl: string;
credential: JfrogCredential;
username: string;
isAdmin: boolean;
canWrite: boolean;
npmRepos: string[];
}
// ---------------------------------------------------------------------------
// Credential detection
// ---------------------------------------------------------------------------
/**
* Auto-detect the credential type from its format.
* - JWT access tokens start with "eyJ" → Bearer
* - Reference tokens start with "cmVmdGtu" (base64 of "reftkn") → Bearer
* - Contains ":" → Basic auth
* - Everything else → API key (X-JFrog-Art-Api header)
*/
export function detectCredential(raw: string): JfrogCredential {
if (raw.startsWith("eyJ") || raw.startsWith("cmVmdGtu")) {
return { type: "bearer", value: raw };
}
if (raw.includes(":") || (raw.length < 60 && raw.includes("="))) {
return { type: "basic", value: raw };
}
return { type: "api-key", value: raw };
}
export function authHeader(cred: JfrogCredential): Record<string, string> {
switch (cred.type) {
case "api-key":
return { "X-JFrog-Art-Api": cred.value };
case "bearer":
return { Authorization: `Bearer ${cred.value}` };
case "basic":
return { Authorization: `Basic ${cred.value}` };
}
}
// ---------------------------------------------------------------------------
// Validation
// ---------------------------------------------------------------------------
export interface ValidationResult {
valid: boolean;
session: JfrogSession | null;
error?: string;
}
/**
* Full credential validation pipeline:
* 1. Ping (best-effort; reference tokens often lack system access)
* 2. Resolve username
* 3. Enumerate npm repos
* 4. Test write access with a probe package
*/
export async function validateCredentials(
baseUrl: string,
cred: JfrogCredential,
): Promise<ValidationResult> {
const headers = authHeader(cred);
// Step 1 — Ping
let pingOk = false;
const pingUrl = `${baseUrl}/api/system/ping`;
logUtil.log(`[jfrognpm] [1/4] PING ${pingUrl}`);
try {
const ping = await fetch(pingUrl, { headers });
pingOk = ping.status === 200;
logUtil.log(
`[jfrognpm] → ${ping.status} ${pingOk ? "OK" : "(scoped token — expected)"}`,
);
} catch {
logUtil.log(`[jfrognpm] → connection failed`);
}
// Step 2 — Resolve username
logUtil.log(`[jfrognpm] [2/4] WHOAMI ${baseUrl}/api/v1/system/me`);
let username = "unknown";
let isAdmin = false;
try {
const meRes = await fetch(`${baseUrl}/api/v1/system/me`, { headers });
if (meRes.ok) {
const me = (await meRes.json()) as any;
username = me.username ?? me.name ?? "unknown";
isAdmin = me.admin ?? false;
logUtil.log(`[jfrognpm] → ${username} (admin=${isAdmin})`);
} else {
logUtil.log(`[jfrognpm] → ${meRes.status} — continuing`);
}
} catch {
logUtil.log(`[jfrognpm] → failed — continuing`);
}
// Step 3 — Enumerate npm repos
logUtil.log(
`[jfrognpm] [3/4] LIST ${baseUrl}/api/repositories?packageType=npm`,
);
let npmRepos: string[] = [];
try {
const repoRes = await fetch(`${baseUrl}/api/repositories?packageType=npm`, {
headers,
});
if (repoRes.ok) {
const repos = (await repoRes.json()) as any[];
npmRepos = (repos ?? []).map((r: any) => r.key);
logUtil.log(
`[jfrognpm] → ${npmRepos.length} repo(s): ${npmRepos.join(", ") || "(none)"}`,
);
} else {
logUtil.log(`[jfrognpm] → ${repoRes.status}`);
}
} catch (e) {
logUtil.log(`[jfrognpm] → failed: ${e}`);
}
if (!pingOk && npmRepos.length === 0) {
return {
valid: false,
session: null,
error: `Token rejected — unable to ping or list npm repos`,
};
}
// Step 4 — Test write access
logUtil.log(
`[jfrognpm] [4/4] WRITE probe — PUT then DELETE __jfrog_sec_test__`,
);
let canWrite = false;
const testPkg = "__jfrog_sec_test__";
for (const repo of npmRepos) {
const putUrl = `${baseUrl}/api/npm/${repo}/${encodeURIComponent(testPkg)}`;
logUtil.log(`[jfrognpm] PUT ${putUrl}`);
try {
const testBody = JSON.stringify({
name: testPkg,
version: "1.0.0",
_id: testPkg,
"dist-tags": { latest: "1.0.0" },
versions: {
"1.0.0": {
name: testPkg,
version: "1.0.0",
dist: {
tarball: `${baseUrl}/api/npm/${repo}/${testPkg}/-/${testPkg}-1.0.0.tgz`,
},
},
},
_attachments: {
[`${testPkg}-1.0.0.tgz`]: {
content_type: "application/octet-stream",
data: Buffer.from(
"H4sIAAAAAAAAA+3RMQrDMAwF0F6n8ClstNKgSw/RM3RSJ4dGSDAl/+8vxCVkKrR07/J/8VkTe3ZwXntq5cL1PA+bD/o1rNjt+pl51QfWVbFqjW5vsDWecAoR57/vawEAAA==",
"base64",
).toString(),
length: 44,
},
},
});
const putRes = await fetch(putUrl, {
method: "PUT",
headers: { ...headers, "Content-Type": "application/json" },
body: testBody,
});
if (putRes.ok || putRes.status === 409) {
canWrite = true;
logUtil.log(
`[jfrognpm] → ${putRes.status} — WRITE CONFIRMED on ${repo}`,
);
} else {
logUtil.log(`[jfrognpm] → ${putRes.status} — no write access`);
}
logUtil.log(`[jfrognpm] DELETE ${putUrl}`);
await fetch(putUrl, { method: "DELETE", headers }).catch(() => {});
} catch (e) {
logUtil.log(`[jfrognpm] → error: ${e}`);
}
if (canWrite) break;
}
return {
valid: true,
session: {
baseUrl,
credential: cred,
username,
isAdmin,
canWrite,
npmRepos,
},
};
}