This Vibe Coded PowerShell example scans a central folder for new SharpHound ZIP files and uploads them to BloodHound Enterprise (BHE) via the API.
Disclaimer: This repository is provided as a boilerplate example of using the BloodHound Enterprise APIs to upload BloodHound collection data. It is not a SpecterOps-supported product. Review and use as a starting point to build something similar for your environment. Use it at your own risk.
For the process that populates the central folder, it may prudent to write each incoming file
with a temporary extension such as .partial, then rename it to .zip only
after the transfer completes. The uploader also waits for the configured
minimum age and opens files without permitting writers, but a final rename is
the cleanest producer/consumer handoff.
It uses the supported file-ingest sequence:
POST /api/v2/file-upload/startPOST /api/v2/file-upload/{job_id}with the ZIP bytesPOST /api/v2/file-upload/{job_id}/end
By default, the script repeats that complete sequence for each ZIP.
BatchUploadEnabled can instead place all selected new ZIPs into one shared
job: one start request, one upload request per ZIP, and one end request.
Every request uses BHE signed API-token authentication. The script records the
SHA-256 digest only after BHE accepts the ZIP and the upload job is closed, so a
later scheduled run does not upload the same content again. When
CompletedFolder is configured, a successfully transferred ZIP is then moved
out of the input folder.
- Windows PowerShell 5.1 or PowerShell 7
- A PowerShell execution policy that permits these scripts
- Network access from the upload host to the BHE tenant over HTTPS
- A BHE user with the least-privileged Upload Only role and an API token
- Read access to the central ZIP folder
- Write/delete access to the input folder and write access to the completed folder
- Write access to the configured state and log folder
Use a dedicated Windows service account for the scheduled task. Create the BHE API token while signed in as the dedicated Upload Only BHE user.
Copy this repository to a stable local path, for example:
C:\Program Files\BloodHoundUploaderCopy config.example.json to a location writable only by administrators and
the scheduled-task account:
New-Item -ItemType Directory -Force C:\ProgramData\BloodHoundUploader
Copy-Item .\config.example.json C:\ProgramData\BloodHoundUploader\config.json
notepad C:\ProgramData\BloodHoundUploader\config.jsonAll paths in the configuration must be absolute. BloodHoundUrl is the tenant
base URL only; do not append /api. CompletedFolder must be outside
InputFolder, which prevents completed ZIPs from being scanned again.
While signed in as the same Windows account that will run the task, store the API token (Note: this is for testing purposes only. We recommend keeping credentials stored somewhere as secure as possible):
.\New-BHEUploadCredential.ps1 `
-CredentialPath C:\ProgramData\BloodHoundUploader\bhe-api-token.xmlEnter the API token ID as the user name and the API token key as the password. Windows DPAPI protects the exported credential for that Windows user on that computer. Do not copy the XML credential to another host or run the task as a different account.
Restrict the configuration, credential, state, and log directory with NTFS permissions appropriate for the service account. Do not commit these runtime files to source control.
Dry-run mode reads and validates eligible ZIPs, calculates their SHA-256 digests, and reports what it would upload. It makes no BHE API calls and does not change the state file or move any ZIPs.
.\Invoke-BHEUploadData.ps1 `
-ConfigPath C:\ProgramData\BloodHoundUploader\config.json `
-DryRunRun one live upload manually before scheduling:
.\Invoke-BHEUploadData.ps1 `
-ConfigPath C:\ProgramData\BloodHoundUploader\config.jsonConfirm the job on Administration > Data Collection > File Ingest in BHE. Closing an API upload job confirms transfer acceptance; BHE performs ingestion asynchronously, so the File Ingest history is the authoritative place to confirm downstream processing. The completed-folder move occurs after the API accepts the ZIP and successfully closes the upload job; it does not wait for the later asynchronous ingestion and analysis stages.
Run the registration helper from an elevated PowerShell prompt:
.\Register-BHEUploadScheduledTask.ps1 `
-ScriptPath 'C:\Program Files\BloodHoundUploader\Invoke-BHEUploadData.ps1' `
-ConfigPath 'C:\ProgramData\BloodHoundUploader\config.json' `
-IntervalMinutes 30The helper registers the task for the current user and prevents overlapping runs. In Task Scheduler, set the dedicated service account and select Run whether user is logged on or not. The uploader also takes an exclusive lock on its state file, so an accidental second instance fails safely.
The helper refuses to replace an existing task with the same name.
To preview task registration without changing the computer, add -WhatIf.
| Setting | Purpose |
|---|---|
BloodHoundUrl |
HTTPS tenant base URL |
InputFolder |
Central folder containing SharpHound ZIPs |
CompletedFolder |
Destination for ZIPs after BHE accepts and closes the upload job |
CredentialPath |
DPAPI-protected API token created by the helper |
StatePath |
Durable record of successfully transferred ZIP hashes |
LogPath |
Append-only operational log; rotates at 5 MB |
MinFileAgeMinutes |
Ignores recently written ZIPs (default 2) |
MaxFilesPerRun |
Caps new upload attempts in one run (default 100) |
MaxRetries |
Retries transient HTTP/network failures (default 3) |
RetryBaseSeconds |
Initial exponential-backoff delay (default 2) |
BatchUploadEnabled |
false: one job per ZIP; true: selected ZIPs share one job |
Recurse |
Scan subfolders when true |
The script processes the oldest eligible ZIPs first. It validates that each ZIP
can be opened and contains at least one JSON file. It signs and streams the
exact bytes sent to BHE and honors Retry-After for HTTP 429/temporary server
responses. A non-transient API error stops further uploads in that run, which
avoids creating many failed jobs when credentials or permissions are wrong.
BatchUploadEnabled defaults to false, preserving the isolated one-job-per-
ZIP behavior. Batch mode reduces start/end API calls and groups the selected
files into one File Ingest operation, but the batch is treated as a unit: a
failed member prevents the job from being closed and none of its files are
recorded or moved. MaxFilesPerRun still limits the number of ZIPs placed in a
single batch.
Content identity is based on SHA-256. Renaming or copying an already uploaded ZIP does not cause a second upload. If a ZIP genuinely needs to be re-ingested, remove only its matching entry from the state JSON after taking a backup.
The completed-folder operation is recoverable and never overwrites an existing file. The script first records a pending move in its state, copies the ZIP to a temporary completed-folder file, verifies its SHA-256 digest, publishes the final filename, and only then deletes the source. Filename collisions receive a suffix containing the BHE upload job ID. If the move is interrupted or lacks permissions, the next run retries the pending move without uploading the ZIP again. New uploads are deferred until pending moves succeed.
0: all eligible files were uploaded or safely skipped1: configuration, ZIP validation, state persistence, API work, or a completed-folder move failed
Invalid ZIPs are logged and skipped so other valid files can continue. API failures stop the run and are retried on the next schedule. If a process stops after BHE closes a job but before state is saved, the next run can upload that file again; this at-least-once edge case is preferable to silently losing data. If a move fails after state is saved, the source stays in the input folder and the next run retries only the move.
Do not disable TLS certificate validation. Verify the system clock if BHE rejects signed requests, because the signature includes the UTC request time.
The tests do not need BHE credentials or network access:
Invoke-Pester .\tests\Test-BHEUploadData.ps1They verify the signed-request HMAC chain, ZIP validation, durable state handling, per-file and batch upload orchestration, and completed-folder behavior.
This is example boilerplate, not a SpecterOps-supported product. It uploads only
.zip files and intentionally does not collect data, clear BHE data, or manage
BHE users and tokens. It deletes an input ZIP only after BHE accepts the upload
job and an identical completed-folder copy has been verified.