Export


Secure60 exports event, signal and threat data as CSV. There are two routes, and which one you use depends on size.

Download from Search returns a CSV to your browser. Use it for a result set you want to open in a spreadsheet.

Background export writes to your own object storage bucket. Use it for anything large — a month of a busy project, an audit pack, a legal hold. The export runs on the platform, survives you closing the browser, and can run for hours or days.


SEARCH Query and filters Time range Destination EXPORT JOB Splits the range into parts Progress in Jobs YOUR BUCKET export_000000.csv.gz export_000001.csv.gz export_manifest.json ONE CSV Concatenate the parts in order Ready to load

One part file per time window, plus a manifest describing all of them.


Run a search, then use Export on the results toolbar. The CSV downloads through your browser with the filters and time range you had applied.

This route holds the connection open while the file is produced, so it suits result sets you would open in a spreadsheet. For anything larger, use a background export.


Background export

A background export is submitted as a job and runs on the platform. You can close the browser; the job keeps going.

Why the output arrives in parts

A large export is not run as one enormous query. The requested time range is divided into windows, and each window is exported separately, one after another. You get one file per window rather than a single file.

This is what makes a long export safe to run and possible to watch:

The parts are sized by row count, not by fixed time. A project that is quiet overnight and busy at 09:00 produces evenly sized parts rather than one enormous file and a row of near-empty ones.

Setting up a destination

Destinations are saved once and re-used. In the export panel choose Add location and provide:

Field Notes
Name How it appears in the destination list
Type S3-compatible object storage, including Amazon S3 and Google Cloud Storage
Bucket URL For example https://storage.googleapis.com/acme-exports/
Access key / secret Credentials with write access to that bucket

The credentials are stored against your organisation and used by the platform to write the export. Exports are written straight into your bucket, so the data lands in storage you control and under your own retention and access rules.

Watching progress

Open Organisation Settings → Jobs. Each export shows a progress bar, the current part and total, rows written against the estimate, and any parts that needed a retry. When the export finishes, the job records the destination, the total rows and the list of files.


What lands in your bucket

For an export named myexport.csv.gz covering four windows:

myexport_000000.csv.gz     <- newest window, contains the header row
myexport_000001.csv.gz
myexport_000002.csv.gz
myexport_000003.csv.gz     <- oldest window
myexport_manifest.json

Two things to know about the numbering:

CONCATENATION ORDER _000000 07:00 → 08:00 UTC HEADER ROW newest window _000001 06:00 → 07:00 UTC data only _000002 05:00 → 06:00 UTC data only _000003 04:00 → 05:00 UTC oldest window newest events oldest events Joining the files in filename order gives one CSV ordered newest first, the same order Search shows.

The manifest

_manifest.json describes the export so you can verify it before loading anything:

{
  "export_location": "https://storage.googleapis.com/acme-exports/myexport.csv.gz",
  "project_id": 301,
  "from_time": 1786947704,
  "to_time": 1786951304,
  "chunk_count": 2,
  "rows_written": 56752,
  "bytes_written": 98221883,
  "parts": [
    {
      "index": 0,
      "file": "myexport_000000.csv.gz",
      "from_utc": "2026-08-17 06:51:44",
      "to_utc": "2026-08-17 07:21:44",
      "has_header": true
    },
    {
      "index": 1,
      "file": "myexport_000001.csv.gz",
      "from_utc": "2026-08-17 06:21:44",
      "to_utc": "2026-08-17 06:51:44",
      "has_header": false
    }
  ]
}

Check chunk_count against the number of files you downloaded before reassembling. A missing part removes that time range from the output without any other sign.


Reassembling the parts

Download the parts and the manifest into one directory, then run the script below. It checks the number of parts against the manifest, refuses to join across a missing part, and skips files that are not numbered parts.

Joining the files by hand with cat myexport_*.csv.gz looks like it should work, and usually does — but it fails quietly in one common case. If your browser has re-downloaded a part, you will have both myexport_000000.csv.gz and myexport_000000 (1).csv.gz in the folder. A shell glob sorts the (1) copy first, so the join silently repeats a block of rows and leaves a second header row in the middle of the file. Nothing reports an error. On a real three-part export that produced 3,395 duplicated rows in a file that still opened without complaint.

Use the script for anything you intend to rely on.

#!/usr/bin/env bash
# Reassemble a Secure60 chunked export into a single CSV.
# Usage: ./export-reassemble.sh <directory> [output-file]
set -euo pipefail

DIR="${1:-}"; OUT="${2:-}"
[ -n "$DIR" ] && [ -d "$DIR" ] || { echo "usage: $0 <directory> [output-file]" >&2; exit 2; }

# Collect the part files in index order. Zero-padded numbering means a plain
# sort is already the correct concatenation order.
PARTS=()
while IFS= read -r p; do PARTS+=("$p"); done < <(
    find "$DIR" -maxdepth 1 -type f \
        \( -name '*_[0-9][0-9][0-9][0-9][0-9][0-9].csv.gz' \
        -o -name '*_[0-9][0-9][0-9][0-9][0-9][0-9].csv' \) | sort)
[ "${#PARTS[@]}" -gt 0 ] || { echo "No export parts found in $DIR" >&2; exit 1; }

# Browsers rename a repeated download to "name (1).csv.gz", which is not a part
# file. Point them out rather than silently ignoring them, so a part that was
# only ever downloaded under its "(1)" name is not mistaken for a missing part.
IGNORED=0
while IFS= read -r x; do IGNORED=$((IGNORED + 1)); done < <(
    find "$DIR" -maxdepth 1 -type f \( -name '*.csv' -o -name '*.csv.gz' \) \
    ! -name '*_[0-9][0-9][0-9][0-9][0-9][0-9].csv.gz' \
    ! -name '*_[0-9][0-9][0-9][0-9][0-9][0-9].csv')
[ "$IGNORED" -eq 0 ] || echo "Note: ignoring $IGNORED file(s) that are not numbered parts (e.g. browser copies like 'name (1).csv.gz')."

BASE="$(basename "${PARTS[0]}")"
STEM="${BASE%_[0-9][0-9][0-9][0-9][0-9][0-9]*}"
COMPRESSED=false; case "${PARTS[0]}" in *.gz) COMPRESSED=true ;; esac

# Verify against the manifest when one was downloaded.
MANIFEST="$DIR/${STEM}_manifest.json"
if [ -f "$MANIFEST" ]; then
    EXPECTED=$(tr -d ' \n' < "$MANIFEST" | sed -n 's/.*"chunk_count":\([0-9]*\).*/\1/p')
    if [ -n "$EXPECTED" ] && [ "$EXPECTED" != "${#PARTS[@]}" ]; then
        echo "Manifest lists $EXPECTED parts, found ${#PARTS[@]}. Download the missing parts first." >&2
        exit 1
    fi
    echo "Manifest OK: ${#PARTS[@]} parts."
else
    echo "No manifest present; joining the ${#PARTS[@]} parts found."
fi

# Refuse to join across a gap - that would drop a time range with no other sign.
INDEX=0
for p in "${PARTS[@]}"; do
    n="$(basename "$p")"; n="${n##*_}"; n="${n%%.*}"
    [ "$((10#$n))" -eq "$INDEX" ] || { echo "Missing part $(printf '%06d' "$INDEX")." >&2; exit 1; }
    INDEX=$((INDEX + 1))
done

[ -n "$OUT" ] || { OUT="$DIR/${STEM}.csv"; $COMPRESSED && OUT="$OUT.gz"; }
: > "$OUT"
for p in "${PARTS[@]}"; do cat "$p" >> "$OUT"; done

echo "Wrote $OUT from ${#PARTS[@]} parts."
if $COMPRESSED; then
    gzip -t "$OUT" && echo "Rows: $(( $(gzip -dc "$OUT" | wc -l) - 1 )) (excluding the header)."
else
    echo "Rows: $(( $(wc -l < "$OUT") - 1 )) (excluding the header)."
fi

Loading the parts straight into a database or analytics tool without joining them also works. Point the loader at the directory and tell it the first file has a header.


Choosing what to export

An export carries whatever the search carried, so build the search first and export it when the results look right.


Format

Exports are gzip-compressed CSV with a header row on the first part. Field values are quoted and escaped, so values containing commas, quotes or newlines survive the round trip.

Event fields are written as a single event column containing the field map. Fields promoted to columns on your deployment are also present as their own columns.

For programmatic access to the same data in JSON, use the Data API rather than an export.


Access control and audit


Things to know

Filenames are yours to choose, and re-using one replaces the previous export. The field is pre-filled with a timestamped name that is unique. If you replace it with a fixed name and run the export twice, the second run overwrites the first in your bucket. Keep the timestamp, or use a distinct name per run, when you need to retain both.

Parts are written as they complete. A part appearing in your bucket does not mean the export has finished. Check the job in the portal, or wait for _manifest.json — the manifest is written last, once every part has been confirmed.

A failed part is retried automatically. If it still fails, the export is marked failed and the job records which part stopped it. Parts that completed remain in your bucket and are listed in the job detail, so a partial export is still usable.

Very large exports take real time. An export is bounded by how much data it has to read and write, not by a timeout. The Jobs view carries an estimated time remaining once the first parts are done.


Scheduled exports

Recurring exports on a schedule are available for enterprise deployments. Contact our team for setup.

Back to top