Skip to content

Repository files navigation

BloodHound Enterprise folder uploader

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:

  1. POST /api/v2/file-upload/start
  2. POST /api/v2/file-upload/{job_id} with the ZIP bytes
  3. POST /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.

Requirements

  • 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.

Setup

Copy this repository to a stable local path, for example:

C:\Program Files\BloodHoundUploader

Copy 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.json

All 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.xml

Enter 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.

Test before uploading

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 `
  -DryRun

Run one live upload manually before scheduling:

.\Invoke-BHEUploadData.ps1 `
  -ConfigPath C:\ProgramData\BloodHoundUploader\config.json

Confirm 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.

Schedule it

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 30

The 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.

Configuration

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.

Exit codes and troubleshooting

  • 0: all eligible files were uploaded or safely skipped
  • 1: 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.

Offline tests

The tests do not need BHE credentials or network access:

Invoke-Pester .\tests\Test-BHEUploadData.ps1

They verify the signed-request HMAC chain, ZIP validation, durable state handling, per-file and batch upload orchestration, and completed-folder behavior.

Scope

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.

Official API references

About

This PowerShell example scans a central folder for new SharpHound ZIP files and uploads them to BloodHound Enterprise (BHE) via the API.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages