Why use it: turn the free, keyless ops into a periodic monitoring job — brand typosquats, new certs in CT log — instead of remembering to run it by hand. What it generates: a workflow in your repo of yours, a committed state file, and JSON artifacts to inspect when there's a finding.
The example below was written fresh for the consumer's case. It is not a copy of
.github/workflows/audit.ymlorsmoke.ymlin the cti-expert repo — those two files gate development of the tool itself (structural drift, install from a clean machine) and do nothing for you. Read them to understand, don't copy.
Which op to choose for CI
Conditions for an op to suit CI: no key needed, no .venv, and the exit code says something meaningful. Three candidates:
| Op | Does what | Cost |
|---|---|---|
impersonate <domain> |
generate + check typosquat variants via crt.sh + DNS | free (adding --fofa/--urlscan costs credit) |
ct-monitor watch --state <file> <pattern> |
poll CT log, filter new certs matching the brand | free |
hash-id, sensitive-paths, email-hygiene |
pure, no network | free |
Offline check before wiring (real run, no network access):
python3 scripts/backend/intel.py impersonate example.com --generate-only --max 5
{"seed":"example.com","label":"example","tld":"com","variants":[
{"domain":"xample.com","technique":"omission"},
{"domain":"eeexample.com","technique":"repetition"},
{"domain":"wxample.com","technique":"replacement"},
{"domain":"wexample.com","technique":"insertion"},
{"domain":"rxample.com","technique":"replacement"}],
"count":5,"truncated":true}
--generate-only is a fully offline mode — use it for the "can CI run it" step without depending on the network.
Minimal example — GitHub Actions
Place in your repo of yours, .github/workflows/brand-watch.yml:
name: brand-watch
on:
schedule:
- cron: '0 6 * * *' # 06:00 UTC mỗi ngày
workflow_dispatch:
permissions:
contents: write # để commit lại file state của ct-monitor
jobs:
watch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
# Lớp stdlib của cti-expert không cần cài gì — clone là chạy.
- name: Fetch cti-expert
run: git clone --depth 1 https://github.com/7onez/cti-expert.git "$RUNNER_TEMP/cti"
- name: Typosquat sweep
run: |
set -euo pipefail
python3 "$RUNNER_TEMP/cti/scripts/backend/intel.py" \
impersonate "${{ vars.BRAND_DOMAIN }}" --pretty > impersonate.json
- name: CT-log watch (state được commit để chỉ báo cái MỚI)
run: |
set -euo pipefail
mkdir -p .brandwatch
python3 "$RUNNER_TEMP/cti/scripts/backend/intel.py" \
ct-monitor watch "${{ vars.BRAND_KEYWORD }}" \
--state .brandwatch/ct-state.json --json > ct-new.json
- uses: actions/upload-artifact@v4
if: always()
with:
name: brand-watch-${{ github.run_number }}
path: |
impersonate.json
ct-new.json
- name: Persist CT state
run: |
git config user.name github-actions
git config user.email github-actions@github.com
git add .brandwatch/ct-state.json
git diff --cached --quiet || git commit -m "chore(brand-watch): cập nhật state CT"
git push
Two intentional details:
--stateis committed back. Without it, every run treats every cert as new and the job becomes a spam generator.- No key in the workflow. Both ops run keyless. The day you add
--fofa/--urlscan, that's when you need a secret — and then remember to reread 02 about the two cost ledgers.
Branch on exit code — read 04 first
Don't write || echo "failed". The dispatcher's exit codes have specific meanings:
set +e
python3 "$CTI/scripts/backend/intel.py" impersonate "$DOMAIN" --pretty > out.json
rc=$?
set -e
case "$rc" in
0) : ;; # chạy được — đọc out.json để quyết định
2) echo "::error::sai tên op — chạy intel.py list"; exit 1 ;;
3) echo "::error::không thấy backend — clone hỏng hoặc thiếu intel_engine"; exit 1 ;;
4) echo "::error::component chưa cài cho op này"; exit 1 ;;
5) echo "::error::interpreter không exec được — đặt INTEL_PY"; exit 1 ;;
*) echo "::error::script engine thoát $rc"; exit 1 ;;
esac
Important: 0 means the op finished running, not means no findings. The verdict is in the JSON, not in the exit code. To gate, read the JSON yourself:
python3 - <<'PY'
import json
d = json.load(open("impersonate.json"))
imp = d["artifacts"]["impersonation"]
print(f'::notice::sinh {imp["generated"]} bien the | '
f'{imp["existing_count"]} da TON TAI | {imp["candidate_count"]} chua dang ky')
confirmed = [p for p in d["pivots"] if p.get("kind") == "impersonation:candidate"]
if confirmed:
print(f"::warning::{len(confirmed)} lookalike da ton tai")
for p in confirmed[:20]:
print(" -", p.get("value"), "|", p.get("confidence"))
PY
The fields used above (artifacts.impersonation.generated / existing_count / candidate_count, and pivots[].kind) come from build_impersonation_result in intel_engine/WebPivot/tools/wp_impersonate.py, not guessing. Important distinction: existing = has DNS resolution or seen in CT/FOFA/urlscan; candidate = generated variants with no evidence yet — that is a watchlist to monitor, not confirmed infrastructure.
What should not go into CI
- Any op in the outbound gate list (07):
engage,engage-harvest,--submit,misp push/publish. Hookactionguard.pydoes not run in CI — it is a Claude Code hook, not a shell one. In CI you only have the gate in the code, and withmisp publishthere is no undo button at all. - Full case pipeline (
pipeline open) with--serp/--whois-reverse/--fofa-full: every run burns real credits, and cron runs forever. - Anything that writes to
intel_engine/cases/then commits — that is investigation data, and RULE 1 exists precisely because of this.