mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-15 03:50:40 +00:00
690 lines
20 KiB
TypeScript
690 lines
20 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import {
|
|
isNonSecretApiKeyMarker,
|
|
isSecretRefHeaderValueMarker,
|
|
} from "../agents/model-auth-markers.js";
|
|
import { normalizeProviderId } from "../agents/model-selection.js";
|
|
import { resolveStateDir, type OpenClawConfig } from "../config/config.js";
|
|
import { coerceSecretRef } from "../config/types.secrets.js";
|
|
import { resolveSecretInputRef, type SecretRef } from "../config/types.secrets.js";
|
|
import { resolveConfigDir, resolveUserPath } from "../utils.js";
|
|
import { runTasksWithConcurrency } from "../utils/run-with-concurrency.js";
|
|
import { iterateAuthProfileCredentials } from "./auth-profiles-scan.js";
|
|
import { createSecretsConfigIO } from "./config-io.js";
|
|
import { listKnownSecretEnvVarNames } from "./provider-env-vars.js";
|
|
import { secretRefKey } from "./ref-contract.js";
|
|
import {
|
|
isProviderScopedSecretResolutionError,
|
|
resolveSecretRefValue,
|
|
resolveSecretRefValues,
|
|
type SecretRefResolveCache,
|
|
} from "./resolve.js";
|
|
import {
|
|
hasConfiguredPlaintextSecretValue,
|
|
isExpectedResolvedSecretValue,
|
|
} from "./secret-value.js";
|
|
import { isNonEmptyString, isRecord } from "./shared.js";
|
|
import { describeUnknownError } from "./shared.js";
|
|
import {
|
|
listAgentModelsJsonPaths,
|
|
listAuthProfileStorePaths,
|
|
listLegacyAuthJsonPaths,
|
|
parseEnvAssignmentValue,
|
|
readJsonObjectIfExists,
|
|
} from "./storage-scan.js";
|
|
import { discoverConfigSecretTargets } from "./target-registry.js";
|
|
|
|
export type SecretsAuditCode =
|
|
| "PLAINTEXT_FOUND"
|
|
| "REF_UNRESOLVED"
|
|
| "REF_SHADOWED"
|
|
| "LEGACY_RESIDUE";
|
|
|
|
export type SecretsAuditSeverity = "info" | "warn" | "error"; // pragma: allowlist secret
|
|
|
|
export type SecretsAuditFinding = {
|
|
code: SecretsAuditCode;
|
|
severity: SecretsAuditSeverity;
|
|
file: string;
|
|
jsonPath: string;
|
|
message: string;
|
|
provider?: string;
|
|
profileId?: string;
|
|
};
|
|
|
|
export type SecretsAuditStatus = "clean" | "findings" | "unresolved"; // pragma: allowlist secret
|
|
|
|
export type SecretsAuditReport = {
|
|
version: 1;
|
|
status: SecretsAuditStatus;
|
|
filesScanned: string[];
|
|
summary: {
|
|
plaintextCount: number;
|
|
unresolvedRefCount: number;
|
|
shadowedRefCount: number;
|
|
legacyResidueCount: number;
|
|
};
|
|
findings: SecretsAuditFinding[];
|
|
};
|
|
|
|
type RefAssignment = {
|
|
file: string;
|
|
path: string;
|
|
ref: SecretRef;
|
|
expected: "string" | "string-or-object";
|
|
provider?: string;
|
|
};
|
|
|
|
type ProviderAuthState = {
|
|
hasUsableStaticOrOAuth: boolean;
|
|
modes: Set<"api_key" | "token" | "oauth">;
|
|
};
|
|
|
|
type SecretDefaults = {
|
|
env?: string;
|
|
file?: string;
|
|
exec?: string;
|
|
};
|
|
|
|
type AuditCollector = {
|
|
findings: SecretsAuditFinding[];
|
|
refAssignments: RefAssignment[];
|
|
configProviderRefPaths: Map<string, string[]>;
|
|
authProviderState: Map<string, ProviderAuthState>;
|
|
filesScanned: Set<string>;
|
|
};
|
|
|
|
const REF_RESOLVE_FALLBACK_CONCURRENCY = 8;
|
|
const ALWAYS_SENSITIVE_MODEL_PROVIDER_HEADER_NAMES = new Set([
|
|
"authorization",
|
|
"proxy-authorization",
|
|
"x-api-key",
|
|
"api-key",
|
|
"apikey",
|
|
"x-auth-token",
|
|
"auth-token",
|
|
"x-access-token",
|
|
"access-token",
|
|
"x-secret-key",
|
|
"secret-key",
|
|
]);
|
|
const SENSITIVE_MODEL_PROVIDER_HEADER_NAME_FRAGMENTS = [
|
|
"api-key",
|
|
"apikey",
|
|
"token",
|
|
"secret",
|
|
"password",
|
|
"credential",
|
|
];
|
|
|
|
function isLikelySensitiveModelProviderHeaderName(value: string): boolean {
|
|
const normalized = value.trim().toLowerCase();
|
|
if (!normalized) {
|
|
return false;
|
|
}
|
|
if (ALWAYS_SENSITIVE_MODEL_PROVIDER_HEADER_NAMES.has(normalized)) {
|
|
return true;
|
|
}
|
|
return SENSITIVE_MODEL_PROVIDER_HEADER_NAME_FRAGMENTS.some((fragment) =>
|
|
normalized.includes(fragment),
|
|
);
|
|
}
|
|
|
|
function addFinding(collector: AuditCollector, finding: SecretsAuditFinding): void {
|
|
collector.findings.push(finding);
|
|
}
|
|
|
|
function collectProviderRefPath(
|
|
collector: AuditCollector,
|
|
providerId: string,
|
|
configPath: string,
|
|
): void {
|
|
const key = normalizeProviderId(providerId);
|
|
const existing = collector.configProviderRefPaths.get(key);
|
|
if (existing) {
|
|
existing.push(configPath);
|
|
return;
|
|
}
|
|
collector.configProviderRefPaths.set(key, [configPath]);
|
|
}
|
|
|
|
function trackAuthProviderState(
|
|
collector: AuditCollector,
|
|
provider: string,
|
|
mode: "api_key" | "token" | "oauth",
|
|
): void {
|
|
const key = normalizeProviderId(provider);
|
|
const existing = collector.authProviderState.get(key);
|
|
if (existing) {
|
|
existing.hasUsableStaticOrOAuth = true;
|
|
existing.modes.add(mode);
|
|
return;
|
|
}
|
|
collector.authProviderState.set(key, {
|
|
hasUsableStaticOrOAuth: true,
|
|
modes: new Set([mode]),
|
|
});
|
|
}
|
|
|
|
function collectEnvPlaintext(params: { envPath: string; collector: AuditCollector }): void {
|
|
if (!fs.existsSync(params.envPath)) {
|
|
return;
|
|
}
|
|
params.collector.filesScanned.add(params.envPath);
|
|
const knownKeys = new Set(listKnownSecretEnvVarNames());
|
|
const raw = fs.readFileSync(params.envPath, "utf8");
|
|
const lines = raw.split(/\r?\n/);
|
|
for (const line of lines) {
|
|
const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
if (!match) {
|
|
continue;
|
|
}
|
|
const key = match[1] ?? "";
|
|
if (!knownKeys.has(key)) {
|
|
continue;
|
|
}
|
|
const value = parseEnvAssignmentValue(match[2] ?? "");
|
|
if (!value) {
|
|
continue;
|
|
}
|
|
addFinding(params.collector, {
|
|
code: "PLAINTEXT_FOUND",
|
|
severity: "warn",
|
|
file: params.envPath,
|
|
jsonPath: `$env.${key}`,
|
|
message: `Potential secret found in .env (${key}).`,
|
|
});
|
|
}
|
|
}
|
|
|
|
function collectConfigSecrets(params: {
|
|
config: OpenClawConfig;
|
|
configPath: string;
|
|
collector: AuditCollector;
|
|
}): void {
|
|
const defaults = params.config.secrets?.defaults;
|
|
for (const target of discoverConfigSecretTargets(params.config)) {
|
|
if (!target.entry.includeInAudit) {
|
|
continue;
|
|
}
|
|
const { ref } = resolveSecretInputRef({
|
|
value: target.value,
|
|
refValue: target.refValue,
|
|
defaults,
|
|
});
|
|
if (ref) {
|
|
params.collector.refAssignments.push({
|
|
file: params.configPath,
|
|
path: target.path,
|
|
ref,
|
|
expected: target.entry.expectedResolvedValue,
|
|
provider: target.providerId,
|
|
});
|
|
if (target.entry.trackProviderShadowing && target.providerId) {
|
|
collectProviderRefPath(params.collector, target.providerId, target.path);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const hasPlaintext = hasConfiguredPlaintextSecretValue(
|
|
target.value,
|
|
target.entry.expectedResolvedValue,
|
|
);
|
|
if (
|
|
target.entry.id === "models.providers.*.headers.*" &&
|
|
!isLikelySensitiveModelProviderHeaderName(target.pathSegments.at(-1) ?? "")
|
|
) {
|
|
continue;
|
|
}
|
|
if (!hasPlaintext) {
|
|
continue;
|
|
}
|
|
addFinding(params.collector, {
|
|
code: "PLAINTEXT_FOUND",
|
|
severity: "warn",
|
|
file: params.configPath,
|
|
jsonPath: target.path,
|
|
message: `${target.path} is stored as plaintext.`,
|
|
provider: target.providerId,
|
|
});
|
|
}
|
|
}
|
|
|
|
function collectAuthStoreSecrets(params: {
|
|
authStorePath: string;
|
|
collector: AuditCollector;
|
|
defaults?: SecretDefaults;
|
|
}): void {
|
|
if (!fs.existsSync(params.authStorePath)) {
|
|
return;
|
|
}
|
|
params.collector.filesScanned.add(params.authStorePath);
|
|
const parsedResult = readJsonObjectIfExists(params.authStorePath);
|
|
if (parsedResult.error) {
|
|
addFinding(params.collector, {
|
|
code: "REF_UNRESOLVED",
|
|
severity: "error",
|
|
file: params.authStorePath,
|
|
jsonPath: "<root>",
|
|
message: `Invalid JSON in auth-profiles store: ${parsedResult.error}`,
|
|
});
|
|
return;
|
|
}
|
|
const parsed = parsedResult.value;
|
|
if (!parsed || !isRecord(parsed.profiles)) {
|
|
return;
|
|
}
|
|
for (const entry of iterateAuthProfileCredentials(parsed.profiles)) {
|
|
if (entry.kind === "api_key" || entry.kind === "token") {
|
|
const { ref } = resolveSecretInputRef({
|
|
value: entry.value,
|
|
refValue: entry.refValue,
|
|
defaults: params.defaults,
|
|
});
|
|
if (ref) {
|
|
params.collector.refAssignments.push({
|
|
file: params.authStorePath,
|
|
path: `profiles.${entry.profileId}.${entry.valueField}`,
|
|
ref,
|
|
expected: "string",
|
|
provider: entry.provider,
|
|
});
|
|
trackAuthProviderState(params.collector, entry.provider, entry.kind);
|
|
}
|
|
if (isNonEmptyString(entry.value)) {
|
|
addFinding(params.collector, {
|
|
code: "PLAINTEXT_FOUND",
|
|
severity: "warn",
|
|
file: params.authStorePath,
|
|
jsonPath: `profiles.${entry.profileId}.${entry.valueField}`,
|
|
message:
|
|
entry.kind === "api_key"
|
|
? "Auth profile API key is stored as plaintext."
|
|
: "Auth profile token is stored as plaintext.",
|
|
provider: entry.provider,
|
|
profileId: entry.profileId,
|
|
});
|
|
trackAuthProviderState(params.collector, entry.provider, entry.kind);
|
|
}
|
|
continue;
|
|
}
|
|
if (entry.hasAccess || entry.hasRefresh) {
|
|
addFinding(params.collector, {
|
|
code: "LEGACY_RESIDUE",
|
|
severity: "info",
|
|
file: params.authStorePath,
|
|
jsonPath: `profiles.${entry.profileId}`,
|
|
message: "OAuth credentials are present (out of scope for static SecretRef migration).",
|
|
provider: entry.provider,
|
|
profileId: entry.profileId,
|
|
});
|
|
trackAuthProviderState(params.collector, entry.provider, "oauth");
|
|
}
|
|
}
|
|
}
|
|
|
|
function collectAuthJsonResidue(params: { stateDir: string; collector: AuditCollector }): void {
|
|
for (const authJsonPath of listLegacyAuthJsonPaths(params.stateDir)) {
|
|
params.collector.filesScanned.add(authJsonPath);
|
|
const parsedResult = readJsonObjectIfExists(authJsonPath);
|
|
if (parsedResult.error) {
|
|
addFinding(params.collector, {
|
|
code: "REF_UNRESOLVED",
|
|
severity: "error",
|
|
file: authJsonPath,
|
|
jsonPath: "<root>",
|
|
message: `Invalid JSON in legacy auth.json: ${parsedResult.error}`,
|
|
});
|
|
continue;
|
|
}
|
|
const parsed = parsedResult.value;
|
|
if (!parsed) {
|
|
continue;
|
|
}
|
|
for (const [providerId, value] of Object.entries(parsed)) {
|
|
if (!isRecord(value)) {
|
|
continue;
|
|
}
|
|
if (value.type === "api_key" && isNonEmptyString(value.key)) {
|
|
addFinding(params.collector, {
|
|
code: "LEGACY_RESIDUE",
|
|
severity: "warn",
|
|
file: authJsonPath,
|
|
jsonPath: providerId,
|
|
message: "Legacy auth.json contains static api_key credentials.",
|
|
provider: providerId,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function collectModelsJsonSecrets(params: {
|
|
modelsJsonPath: string;
|
|
collector: AuditCollector;
|
|
}): void {
|
|
if (!fs.existsSync(params.modelsJsonPath)) {
|
|
return;
|
|
}
|
|
params.collector.filesScanned.add(params.modelsJsonPath);
|
|
const parsedResult = readJsonObjectIfExists(params.modelsJsonPath);
|
|
if (parsedResult.error) {
|
|
addFinding(params.collector, {
|
|
code: "REF_UNRESOLVED",
|
|
severity: "error",
|
|
file: params.modelsJsonPath,
|
|
jsonPath: "<root>",
|
|
message: `Invalid JSON in models.json: ${parsedResult.error}`,
|
|
});
|
|
return;
|
|
}
|
|
const parsed = parsedResult.value;
|
|
if (!parsed || !isRecord(parsed.providers)) {
|
|
return;
|
|
}
|
|
for (const [providerId, providerValue] of Object.entries(parsed.providers)) {
|
|
if (!isRecord(providerValue)) {
|
|
continue;
|
|
}
|
|
const apiKey = providerValue.apiKey;
|
|
if (coerceSecretRef(apiKey)) {
|
|
addFinding(params.collector, {
|
|
code: "REF_UNRESOLVED",
|
|
severity: "error",
|
|
file: params.modelsJsonPath,
|
|
jsonPath: `providers.${providerId}.apiKey`,
|
|
message: "models.json contains an unresolved SecretRef object; regenerate models.json.",
|
|
provider: providerId,
|
|
});
|
|
} else if (isNonEmptyString(apiKey) && !isNonSecretApiKeyMarker(apiKey)) {
|
|
addFinding(params.collector, {
|
|
code: "PLAINTEXT_FOUND",
|
|
severity: "warn",
|
|
file: params.modelsJsonPath,
|
|
jsonPath: `providers.${providerId}.apiKey`,
|
|
message: "models.json provider apiKey is stored as plaintext.",
|
|
provider: providerId,
|
|
});
|
|
}
|
|
|
|
const headers = isRecord(providerValue.headers) ? providerValue.headers : undefined;
|
|
if (!headers) {
|
|
continue;
|
|
}
|
|
for (const [headerKey, headerValue] of Object.entries(headers)) {
|
|
const headerPath = `providers.${providerId}.headers.${headerKey}`;
|
|
if (coerceSecretRef(headerValue)) {
|
|
addFinding(params.collector, {
|
|
code: "REF_UNRESOLVED",
|
|
severity: "error",
|
|
file: params.modelsJsonPath,
|
|
jsonPath: headerPath,
|
|
message:
|
|
"models.json contains an unresolved SecretRef object for provider headers; regenerate models.json.",
|
|
provider: providerId,
|
|
});
|
|
continue;
|
|
}
|
|
if (!isNonEmptyString(headerValue)) {
|
|
continue;
|
|
}
|
|
if (isSecretRefHeaderValueMarker(headerValue)) {
|
|
continue;
|
|
}
|
|
if (!isLikelySensitiveModelProviderHeaderName(headerKey)) {
|
|
continue;
|
|
}
|
|
addFinding(params.collector, {
|
|
code: "PLAINTEXT_FOUND",
|
|
severity: "warn",
|
|
file: params.modelsJsonPath,
|
|
jsonPath: headerPath,
|
|
message: "models.json provider header value is stored as plaintext.",
|
|
provider: providerId,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
async function collectUnresolvedRefFindings(params: {
|
|
collector: AuditCollector;
|
|
config: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
}): Promise<void> {
|
|
const cache: SecretRefResolveCache = {};
|
|
const refsByProvider = new Map<string, Map<string, SecretRef>>();
|
|
for (const assignment of params.collector.refAssignments) {
|
|
const providerKey = `${assignment.ref.source}:${assignment.ref.provider}`;
|
|
let refsForProvider = refsByProvider.get(providerKey);
|
|
if (!refsForProvider) {
|
|
refsForProvider = new Map<string, SecretRef>();
|
|
refsByProvider.set(providerKey, refsForProvider);
|
|
}
|
|
refsForProvider.set(secretRefKey(assignment.ref), assignment.ref);
|
|
}
|
|
|
|
const resolvedByRefKey = new Map<string, unknown>();
|
|
const errorsByRefKey = new Map<string, unknown>();
|
|
|
|
for (const refsForProvider of refsByProvider.values()) {
|
|
const refs = [...refsForProvider.values()];
|
|
const provider = refs[0]?.provider;
|
|
try {
|
|
const resolved = await resolveSecretRefValues(refs, {
|
|
config: params.config,
|
|
env: params.env,
|
|
cache,
|
|
});
|
|
for (const [key, value] of resolved.entries()) {
|
|
resolvedByRefKey.set(key, value);
|
|
}
|
|
continue;
|
|
} catch (err) {
|
|
if (provider && isProviderScopedSecretResolutionError(err)) {
|
|
for (const ref of refs) {
|
|
errorsByRefKey.set(secretRefKey(ref), err);
|
|
}
|
|
continue;
|
|
}
|
|
// Fall back to per-ref resolution for provider-specific pinpoint errors.
|
|
}
|
|
|
|
const tasks = refs.map(
|
|
(ref) => async (): Promise<{ key: string; resolved: unknown }> => ({
|
|
key: secretRefKey(ref),
|
|
resolved: await resolveSecretRefValue(ref, {
|
|
config: params.config,
|
|
env: params.env,
|
|
cache,
|
|
}),
|
|
}),
|
|
);
|
|
const fallback = await runTasksWithConcurrency({
|
|
tasks,
|
|
limit: Math.min(REF_RESOLVE_FALLBACK_CONCURRENCY, refs.length),
|
|
errorMode: "continue",
|
|
onTaskError: (error, index) => {
|
|
const ref = refs[index];
|
|
if (!ref) {
|
|
return;
|
|
}
|
|
errorsByRefKey.set(secretRefKey(ref), error);
|
|
},
|
|
});
|
|
for (const result of fallback.results) {
|
|
if (!result) {
|
|
continue;
|
|
}
|
|
resolvedByRefKey.set(result.key, result.resolved);
|
|
}
|
|
}
|
|
|
|
for (const assignment of params.collector.refAssignments) {
|
|
const key = secretRefKey(assignment.ref);
|
|
const resolveErr = errorsByRefKey.get(key);
|
|
if (resolveErr) {
|
|
addFinding(params.collector, {
|
|
code: "REF_UNRESOLVED",
|
|
severity: "error",
|
|
file: assignment.file,
|
|
jsonPath: assignment.path,
|
|
message: `Failed to resolve ${assignment.ref.source}:${assignment.ref.provider}:${assignment.ref.id} (${describeUnknownError(resolveErr)}).`,
|
|
provider: assignment.provider,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (!resolvedByRefKey.has(key)) {
|
|
addFinding(params.collector, {
|
|
code: "REF_UNRESOLVED",
|
|
severity: "error",
|
|
file: assignment.file,
|
|
jsonPath: assignment.path,
|
|
message: `Failed to resolve ${assignment.ref.source}:${assignment.ref.provider}:${assignment.ref.id} (resolved value is missing).`,
|
|
provider: assignment.provider,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const resolved = resolvedByRefKey.get(key);
|
|
if (!isExpectedResolvedSecretValue(resolved, assignment.expected)) {
|
|
addFinding(params.collector, {
|
|
code: "REF_UNRESOLVED",
|
|
severity: "error",
|
|
file: assignment.file,
|
|
jsonPath: assignment.path,
|
|
message:
|
|
assignment.expected === "string"
|
|
? `Failed to resolve ${assignment.ref.source}:${assignment.ref.provider}:${assignment.ref.id} (resolved value is not a non-empty string).`
|
|
: `Failed to resolve ${assignment.ref.source}:${assignment.ref.provider}:${assignment.ref.id} (resolved value is not a string/object).`,
|
|
provider: assignment.provider,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function collectShadowingFindings(collector: AuditCollector): void {
|
|
for (const [provider, paths] of collector.configProviderRefPaths.entries()) {
|
|
const authState = collector.authProviderState.get(provider);
|
|
if (!authState?.hasUsableStaticOrOAuth) {
|
|
continue;
|
|
}
|
|
const modeText = [...authState.modes].join("/");
|
|
for (const configPath of paths) {
|
|
addFinding(collector, {
|
|
code: "REF_SHADOWED",
|
|
severity: "warn",
|
|
file: "openclaw.json",
|
|
jsonPath: configPath,
|
|
message: `Auth profile credentials (${modeText}) take precedence for provider "${provider}", so this config ref may never be used.`,
|
|
provider,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function summarizeFindings(findings: SecretsAuditFinding[]): SecretsAuditReport["summary"] {
|
|
return {
|
|
plaintextCount: findings.filter((entry) => entry.code === "PLAINTEXT_FOUND").length,
|
|
unresolvedRefCount: findings.filter((entry) => entry.code === "REF_UNRESOLVED").length,
|
|
shadowedRefCount: findings.filter((entry) => entry.code === "REF_SHADOWED").length,
|
|
legacyResidueCount: findings.filter((entry) => entry.code === "LEGACY_RESIDUE").length,
|
|
};
|
|
}
|
|
|
|
export async function runSecretsAudit(
|
|
params: {
|
|
env?: NodeJS.ProcessEnv;
|
|
} = {},
|
|
): Promise<SecretsAuditReport> {
|
|
const env = params.env ?? process.env;
|
|
const io = createSecretsConfigIO({ env });
|
|
const snapshot = await io.readConfigFileSnapshot();
|
|
const configPath = resolveUserPath(snapshot.path);
|
|
const defaults = snapshot.valid ? snapshot.config.secrets?.defaults : undefined;
|
|
|
|
const collector: AuditCollector = {
|
|
findings: [],
|
|
refAssignments: [],
|
|
configProviderRefPaths: new Map(),
|
|
authProviderState: new Map(),
|
|
filesScanned: new Set([configPath]),
|
|
};
|
|
|
|
const stateDir = resolveStateDir(env, os.homedir);
|
|
const envPath = path.join(resolveConfigDir(env, os.homedir), ".env");
|
|
const config = snapshot.valid ? snapshot.config : ({} as OpenClawConfig);
|
|
|
|
if (snapshot.valid) {
|
|
collectConfigSecrets({
|
|
config,
|
|
configPath,
|
|
collector,
|
|
});
|
|
for (const authStorePath of listAuthProfileStorePaths(config, stateDir)) {
|
|
collectAuthStoreSecrets({
|
|
authStorePath,
|
|
collector,
|
|
defaults,
|
|
});
|
|
}
|
|
for (const modelsJsonPath of listAgentModelsJsonPaths(config, stateDir)) {
|
|
collectModelsJsonSecrets({
|
|
modelsJsonPath,
|
|
collector,
|
|
});
|
|
}
|
|
await collectUnresolvedRefFindings({
|
|
collector,
|
|
config,
|
|
env,
|
|
});
|
|
collectShadowingFindings(collector);
|
|
} else {
|
|
addFinding(collector, {
|
|
code: "REF_UNRESOLVED",
|
|
severity: "error",
|
|
file: configPath,
|
|
jsonPath: "<root>",
|
|
message: "Config is invalid; cannot validate secret references reliably.",
|
|
});
|
|
}
|
|
|
|
collectEnvPlaintext({
|
|
envPath,
|
|
collector,
|
|
});
|
|
collectAuthJsonResidue({
|
|
stateDir,
|
|
collector,
|
|
});
|
|
|
|
const summary = summarizeFindings(collector.findings);
|
|
const status: SecretsAuditStatus =
|
|
summary.unresolvedRefCount > 0
|
|
? "unresolved"
|
|
: collector.findings.length > 0
|
|
? "findings"
|
|
: "clean";
|
|
|
|
return {
|
|
version: 1,
|
|
status,
|
|
filesScanned: [...collector.filesScanned].toSorted(),
|
|
summary,
|
|
findings: collector.findings,
|
|
};
|
|
}
|
|
|
|
export function resolveSecretsAuditExitCode(report: SecretsAuditReport, check: boolean): number {
|
|
if (report.summary.unresolvedRefCount > 0) {
|
|
return 2;
|
|
}
|
|
if (check && report.findings.length > 0) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|