Files
SpecterOps-Nemesis/docs/performance.md
T
Will Schroeder 79682c8aa9 Titus scanner + EVTX parsing (#105)
* Conversion of NoseyParker scanner to Titus

- Old `noseyparker_scanner` project removed, new `titus_scaner` Golang project built
- Replaced `noseyparker` references with `titus` as appropriate
- Bumped various built-in docker image versions (Dapr, Gotenberg, etc.)

* Replace ZIP-only extraction with Titus library archive support

Replace custom ZIP extraction code with Titus library's enum.ExtractText()
to support scanning secrets inside zip, jar, war, ear, apk, ipa, xpi, crx,
tar, tar.gz/tgz, and 7z archives. Document formats (xlsx, docx, pdf, etc.)
are intentionally excluded since Nemesis handles those via document_conversion.

- Add original_path to TitusInput so Titus can dispatch by file extension
  (MinIO stores files by UUID without extensions)
- Add magic-byte fallback detection for ZIP, 7z, gzip, and tar
- Preserve git-repo-in-ZIP detection as a special case before archive scan
- Rename env vars: DECOMPRESS_ZIPS -> EXTRACT_ARCHIVES,
  MAX_EXTRACT_SIZE_MB -> EXTRACT_MAX_TOTAL_SIZE_MB, and add
  EXTRACT_MAX_FILE_SIZE_MB and EXTRACT_MAX_DEPTH
- Add unit tests for archive detection and config loading

* Integrated (optional) Titus secret validation

- Integrated (optional) Titus secret validation that can be enabled

* Added Titus rule ID display and ability to disable specific rules

- Added Titus rule ID display and ability to disable specific built-in rules (by ID)

* `titus_scanner` fixes

- One scanner per concurrent file to prevent timeouts
- Fix custom rule regexes to prevent timeouts
- Exclude a few default run-away rules causing timeouts
- Bumped cores for titus scanner to 2

* Enable Vectorscan/Hyperscan acceleration for titus_scanner

Switch from the pure-Go regexp2 engine to the Vectorscan (Hyperscan)
SIMD-accelerated engine, using a fork with parallelized regexp2
confirmation and content-based deduplication

Key changes:
- Dockerfile: install libvectorscan-dev/libvectorscan5, build with
  CGO_ENABLED=1 and -tags vectorscan
- go.mod: replace praetorian-inc/titus with HarmJ0y/titus fork that
  parallelizes the regexp2 confirmation loop and uses content-based
  dedup (matching portable engine behavior)
- Add bounded-concurrency semaphore in handler to prevent OOM from
  unbounded goroutines while still returning 200 to Dapr immediately
- Add content-based deduplication, match count cap, snippet truncation,
  and payload size logging to prevent oversized publishes
- Replace O(N*M) line/column computation with precomputed line index
  using binary search
- Add GOMEMLIMIT and memory limit to container resources

* Update README.md

* remove `noseyparker_scanner` container

* Add EVTX (Windows Event Log) enrichment module

Parses .evtx files and extracts security-relevant data:
- Markdown summary transform with event counts, timeline, unique accounts/IPs
- Downloadable CSV transforms for account changes, group membership changes,
  explicit credential use (4648), process creation (4688), and task changes
- Findings for high-signal events: audit log cleared, new services, admin group
  changes, account changes, explicit creds, scheduled tasks, PS script blocks
- PowerShell 4104 script blocks reassembled and resubmitted to enrichment
  pipeline as child .ps1 files for full Titus/YARA scanning

- Added de-duping for duplicate PS scripts carved from EVTXs (hash-based)

* EVTX mods

- Power timelining done for SYSTEM
- Inbound/outbound auth for SECURITY
- Filtering for default system accounts
- CSV viewer now can download files, transforms for EVTX transformed to the viewer

* Convert titus scanner to Dapr bulk subscribe for batch processing

- Add bulk subscribe types to models (BulkMessagePayload, BulkResponse, etc.)
- Replace async single-event handler with synchronous HandleBulkEvent
- Process batches with bounded concurrency via semaphore + WaitGroup
- Handle edge cases: duplicate entryId, empty objectID/entryId, panic recovery
- Add BulkMaxMessages and BulkMaxAwaitDurationMs config with clamping
- Set app-max-concurrency to 1 (parallelism within work batch via scanner pool)

---------

Co-authored-by: Lee Chagolla-Christensen <lee@localhost>
2026-03-03 15:34:56 -08:00

8.9 KiB

Nemesis Performance Tuning

This document details different ways to monitor and tune Nemesis's performance. Nemesis performs differently depending on a variety of factors, including the host's architecture and resources (particularly CPU, RAM, and disk speed) and the workload (e.g. the # of files and imbalances in the number of documents, .NET assemblies, source code, etc.).

If workflows begin to fail, or you are experiencing major performance issues, there are a few tunable settings that can help. Alternatively, if your performance is fine already and you want to potentially increase performance more or potentially reduce CPU/RAM usage (to save $$$), you can adjust these values. This document primarily focuses on increasing performance, but you can of course adjust the settings down to decrease resources.

Hardware Resourcing

The first thing to check is if Nemesis has enough hardware resources.

CPU

Under load, monitor CPU usage (e.g. with top/htop or the "Node Exporter" Grafana dashboard (if monitoring is enabled in Nemesis). If all cores are at lower utilization or not maxed out, continue following through this guide. Otherwise, you'll need to increase CPU resources for Nemesis since Nemesis is primarily CPU bound.

RAM

Under load, monitor RAM usage (e.g. with top/htop, free -h, or the "Node Exporter" Grafana dashboard if monitoring is enabled in Nemesis). Ensure that all memory is not being used; otherwise, you will need to increase RAM.

Note that Nemesis will buffer/cache memory if it can. Minio in particular will use any available RAM to cache file data in RAM. This memory is reclaimable, and therefore is still useable by other services/applications. We recommend having at least 1Gb of cache memory available. More may improve performance, but for the most part Nemesis is CPU bound, not RAM bound. You can apply docker compose memory limits to specific services if you want to constrain how much RAM minio consumes.

Disk

The requirements will vary widely here depending on your workload size. A general rule of thumb is 3x the size of all the files being uploaded. Use SSDs if possible.

Analyzing Your Workload

Analyzing Queues

Normally people realize Nemesis isn't going fast enough after uploading a bunch of files and it taking forever to process. Usually this is indicative that files get queued up for processing, but aren't processed fast enough. You can confirm this by analyzing the message queues in Nemesis/RabbitMQ.

In RabbitMQ, Ready counts signify messages waiting to be processed and the delivery / get rates (messages per second) will give you an idea of the processing speed. The following table maps the service to queue mappings:

Docker Service Queue Name Description
file_enrichment files-new_file Uploaded files that haven't begun processing
document_conversion files-document_conversion_input Files waiting to go through document_conversion (strings, text extraction, PDF conversion)
dotnet_service dotnet-dotnet_input Files waiting for .NET decompilation and inspect assembly
titus-scanner titus-titus_input Files waiting to be scanned by titus

If the queue message rates are too slow, you can adjust some settings to try and increase performance. The following sections detail the best bang-for-the-buck service-specific adjustments you can make.

file_enrichment

Every uploaded file is first placed on the files-new_file queue. The file_enrichment service consumes files from the queue and processes each one with the applicable enrichment modules. To improve file_enrichment performance, analyze its CPU usage with docker compose stats file-enrichment or in the "Docker Monitoring" dashboard in Grafana.

The first thing to tune is making sure file_enrichment is efficiently using a single core (currently, the file_enrichment service does not take full advantage of parallelism). Good utilization will look like ~90-110% CPU usage. i.e. the worker thread is taking full advantage of a single core. If CPU utilization is low, increase the number of workers with the ENRICHMENT_MAX_PARALLEL_WORKFLOWS environment variable (default is 5, meaning 5 workers). You'll also want to make sure this isn't set too high, causing workers to compete for CPU amongst themselves. If you increase to ~100 workers, then you'll also need to adjust Dapr's RabbitMQ prefetchCount count in file.yaml.

If additional cores are available, you can scale the file_enrichment container by adding replicas. Do this by modifying both the compose.yaml and compose.prod.yaml files, uncommenting the disabled file-enrichment-### placeholder replicas therein. Feel free to add more by following the same pattern, if wanted.

document_conversion

Every file is added to the files-document_conversion_input queue. The document_conversion service consumes files from the queue and extracts text, runs strings on the file, and converts documents to PDFs. To improve document_conversion performance, analyze its CPU usage with docker compose stats document-conversion or in the "Docker Monitoring" dashboard in Grafana. The document_conversion service can take full advantage of parallelism (so adding replicas is not necessary since a single instance can utilize multiple cores). However, the compose.yaml has resource limits that restrict the document-conversion service to 2 cores by default (adjust it if needed). In addition, you can adjust the DOCUMENTCONVERSION_MAX_PARALLEL_WORKFLOWS environment variable to adjust the number of workers (2 workers by default).

titus-scanner

Every text file is added to the titus-titus_input queue. The titus-scanner service consumes files from the queue and scans them with titus. To improve titus-scanner performance, analyze its CPU usage with docker compose stats titus-scanner or in the "Docker Monitoring" dashboard in Grafana. The titus-scanner service can take full advantage of parallelism (so adding replicas is not necessary since a single instance can utilize multiple cores). However, the compose.yaml has resource limits that restrict the titus-scanner service to 2 cores by default (adjust it if needed). In addition, you can adjust the TITUS_MAX_CONCURRENT_FILES environment variable to adjust the number of workers (2 workers by default). It's recommended to set TITUS_MAX_CONCURRENT_FILES to the same number of cores given to the service.

Dapr Scaling

Nemesis uses Dapr Workflows to build durable and reliable enrichment pipelines. Underneath, the workflows are managed by Dapr's scheduler service, which shares usage of the Postgres database with Nemesis.

You may need to scale the Dapr infrastructure if you considerably increase the performance of the file_enrichment and/or document_conversion services. Scaling Dapr is beyond the scope of this document, but here's some indicators when you may need to:

  • Significant sustained CPU usage (> 80%-90%) by the scheduler container and/or Postgres container.
  • Workflows begin failing frequently.
  • You notice frequent activity failures/retries in Jaeger traces.

If this is the case, first try increasing the number of scheduler instances (example). Dapr does not support more than 3 scheduler instances unless you migrate to using an external etcd store. If Postgres begins to be the bottleneck, you may consider using a separate Postgres instance to store Dapr state.

Additional resources: