SuperAPI AI Directory

dym-cti-expert-contributor-develop-and-gate

Security & auditing

View repository

Updated 10-18 01:21

About this skill

Skill: dym-cti-expert-contributor-develop-and-gate — Contribution branch: modify cti-expert itself

Why use it: the repo has 6 hard rules and a vendored engine; violating them usually fails silently rather than turning tests red. What it produces: a change that passes scripts/audit.sh and the entire test suite without lowering the safety bar.

This file is independent of 01–08. But reading 04 first helps — the DISPATCH table you must update here is exactly what that file describes.


Install the dev environment

git clone https://github.com/7onez/cti-expert.git && cd cti-expert
bash scripts/install-hooks.sh          # hook git pre-commit → leakcheck.sh (BẮT BUỘC mỗi clone)
uv venv && uv pip install -r requirements.txt    # cho §9 của audit + engine test cần SDK

.git/hooks/ is not tracked, so install-hooks.sh must be re-run on every fresh clone. Without it, RULE 1 has only one layer left.

Single gate: scripts/audit.sh

bash scripts/audit.sh

Run it for real without .venv → AUDIT: clean, rc=0. It gates 9 items:

§ What it checks
1 leak check RULE 1, two passes: diff (what this commit ADDS) + tree (every tracked text file)
2 every op DISPATCH resolves to a real script (RULE 3)
3–4 the shim collector is still a re-export, not turned back into a copy (RULE 4)
5 the number of @tool matches the figure recorded in CLAUDE.md
6 py_compile + tests at repo root
7 every hook path that hooks.json registers still exists
8 test the vendored engine, stdlib-only set (15 files)
9 engine tests require claude_agent_sdk — SKIP if there is no .venv

§1 has two passes because they answer two different questions. The diff pass only sees lines someone just touched; a leaked value sitting in a file nobody opens will live forever — 4 values survived for months in exactly that way. The diff pass also only inspects addedlines: rejecting even - lines used to make the gate lock itself (the only way to remove a leaked value is --no-verify, i.e. the gate pushes you back onto the very shortcut it was built to block).

§9 being SKIPped is a real trap. Locally it prints:

== 9. vendored-engine tests needing claude_agent_sdk ==
  SKIPPED (claude_agent_sdk not importable) — 5 tests incl. the context-governor check.

CI has its own job (engine-tests in .github/workflows/audit.yml) and that job fails if it sees a SKIPPED line — because a guard that is silently skipped is not a guard.

Run tests manually

for t in tests/test_*.py; do python3 "$t" >/dev/null 2>&1 && echo "PASS $t" || echo "FAIL $t"; done

10 files at the repo root, all zero-dep (no pytest needed). Run them for real, all 10 rc=0:

Test What it asserts
test_hooks.py 58 checks — the hook fires on the right target, stays silent in safe cases, both fail-open, plugin wiring is machine-independent
test_references.py 580 checks — data files are documented, consumers load real data, a broken file reports loudly
test_no_sample_submission.py 30 checks — no sample-submission path is left ungated
test_indicator_classification.py RULE 5 — 16 placeholders, 7 real names, 8 privacy names, 7 managed nameservers, 3 self-hosted nameservers
test_collect_core.py egress gate, cache reuse, --force, fan-out, the collect→ingest→correlate loop
test_cluster_corroboration.py · test_social_noise_classification.py cluster merge/split logic
test_email_permute.py · test_email_hygiene_nullmx.py · test_osint_tools.py local-only tools

The vendored engine has its own suites: intel_engine/tests/ (test_tool_registry for RULE 2, test_tool_gate for submission approval, test_engage, …). run_eval.py puts both directories on the path so neither suite silently stops running.

Six rules and the cost of breaking them

RULE 1 — never put case data into the skill

Tracked files contain only code and tradecraft. No real people's names, case infrastructure, real GA4/GTM/GSC identifiers, case IDs, or operator PII — including in comments, fixtures, or "real-world examples". Investigation data lives only in intel_engine/cases/, intel_engine/knowledge/, intel_engine/MEMORY/, .env (all gitignored).

The only narrow exception: the curated false-positive ledger (/reference add) — storing indicator values is its purpose, and it lives in the gitignored KB.

When a tracked file needs an example, use: example.com, registrant@example.com, Operator A, G-XXXXXXXXXX, GTM-XXXXXXX, UA-100000001, CASE-0001, 1ExampleBitcoinAddressDoNotUse. Generic public constants (registrar addresses, CDN/ASN ranges, Sedo/Wix favicon hashes) are allowed — they describe tooling rather than a case.

RULE 2 — exactly ONE case store

$INTEL_HOME/cases/<CASE-ID>/. Do not create cases/ at the repository root: being gitignored makes it seem harmless, but kb_ingest will read an empty store and report "no shared indicators" instead of an error.

RULE 3 — every new tool must be registered on the MCP surface

Adding a tool requires all four steps:

  1. Wrap it as @tool(name, description, {params}) in intel_engine/harness/tools.py — mcp_server.py discovers it automatically, with no second edit needed.
  2. Add a row to DISPATCH in scripts/backend/intel.py so the Tier 2 CLI can reach it.
  3. Update the @tool count in CLAUDE.md (currently 78; audit.sh §5 checks the match). Verify: grep -c '@tool(' intel_engine/harness/tools.py.
  4. If it is a new command in SKILL.md §3, it must map to a real DISPATCH operation or a real @tool . A documented command with nothing to execute is worse than having no command.

Adding a mode to an existing tool does not create a new @tool — only extend its description.

Smoke check:

.venv/bin/python intel_engine/harness/mcp_server.py   # gửi JSON-RPC tools/list, xác nhận tool có mặt

In Claude Code, verify with /mcp. Note that Claude Code caches the tool list when connecting — an old registration silently exposes an outdated surface; that is exactly why sessionguard.py warns when the @tool count changes.

RULE 4 — wrapper/collector drift must heal automatically

Five collectors exist in both layers. Each has one canonical file and one nine-line re-export shim (Windows-safe, no symlinks):

collector canonical (edit here) shim (do not edit)
pivot_extract, cdn_ranges, graph_build, wayback_ga scripts/webpivot/ intel_engine/WebPivot/tools/
whois_enrich intel_engine/WebPivot/tools/ scripts/webpivot/

Never turn a shim back into a copy. If directories move, update the relative depth in the shim.

The harness wrapper probes --help for each collector and filters unsupported flags (_supported_flags/_filter_args). Dropped flags are reported in the tool result — deliberately. A dropped --submit/--archive-missing means the evidence was NOT archived; do not silence that warning.

RULE 5 — indicator-classification changes require tests

Two files govern clustering:

  • intel_engine/tools/kb/noise_filters.py — MANAGED_DNS_SUFFIXES, parking favicons, and parking hosts. Maintenance model: add providers as they are encountered.
  • intel_engine/tools/kb/hypothesize.py — _tier(), classifying a relationship as attribution / corroborating / noise.

uses_nameserver is conditional: delegation to a managed provider is noise; delegation to an operator-run nameserver is attribution evidence (you cannot point a domain at ns1.<host-của-họ> without controlling that zone). Adding a provider to MANAGED_DNS_SUFFIXES therefore weakens clustering — deliberately, which is why this list matters.

Every change to these two files must include classification checks covering at least one managed provider and one self-hosted nameserver.

RULE 6 — the harness layer is part of the skill

hooks/ + .claude-plugin/plugin.json is where the two safety properties are actually enforced under Claude Code. Details are in 07. Three points for code contributors:

  • leakguard.py must not reimplement the regex patterns — it shells out to leakcheck.sh. A second copy will drift, and a drifting guard always reports clean.
  • Adding an outbound capability means adding a row to hooks/references/outbound_actions.json, rather than merely adding a confirmation flag in Python.
  • Do not loosen a guard to suppress a prompt. Prefer tightening flag_required or matching exactly. The trailing space in intel.py engage (which permits engage-report) is the pattern to follow.

Syncing the vendored engine — a three-way merge, not a copy

Upstream: the working tree intelligence_assist (GitHub 0xdefh/Intelligence-AS). Synchronization is one-way; cti-expert never writes back.

"Copy and reapply the five shims" is not enough. The vendored tree also contains more than a dozen files intentionally patched by cti-expert, and overwriting them breaks things silently — collectors still run but stop finding anything. Two known examples: wp_common goes up one additional directory level to find .env (cti-expert is nested more deeply; the upstream version resolves all API keys to empty values), and pivot_extract enables reverse WHOIS by default, with --no-whois-reverse as an opt-out (upstream does the reverse).

Find the divergence point using blob identities, rather than memory: index every (blob, path) pair ever committed upstream, plus the current working tree, then hash each vendored file.

vendored blob meaning action
matches an upstream blob at the same path unmodified copy safe to overwrite
path exists upstream, but its blob matches none of them cti-expert patch three-way merge
path does not exist upstream cti-expert only must survive; never rsync --delete

For each patch, choose the merge base with the smallest diff distance across that path's blob history, then git merge-file <ours> <base> <theirs>. Expect duplicate @tool/def blocks where the base predates a tool that both sides later added:

grep -oE '^async def [a-z_]+' <file> | sort | uniq -d

Three remaining traps:

  • Exclude .venv, __pycache__, cases/, knowledge/, MEMORY/, .env.
  • Rename SKILL.md in each component to SKILL.reference.md — the repository has exactly one SKILL.md, at the root.
  • zsh does not split unquoted parameters into words — an rsync $EXCLUDES assembled as one string excludes nothing, pulling the entire 500 MB .venv into the tree.

After syncing, verify all five things:

bash scripts/audit.sh
for t in tests/test_*.py; do python3 "$t" >/dev/null && echo "PASS $t"; done
for t in intel_engine/tests/test_*.py; do python3 "$t" >/dev/null 2>&1 && echo "PASS $t"; done
.venv/bin/python intel_engine/harness/mcp_server.py    # gửi tools/list
python3 scripts/backend/intel.py pipeline open <CASE> <seeds>

Repository CI (read to understand, do not copy)

Workflow What it gates
audit.yml job structural bash scripts/audit.sh — standard library/grep, no virtual environment, runs in seconds
audit.yml job engine-tests installs requirements.txt and reruns the audit; fails if §9 reports SKIPPED
audit.yml job leak-diff PRs only — scans only ADDED lines against the base ref, so example values already in the tree are not flagged again
smoke.yml clone→install→run in a minimal Ubuntu container (root, no sudo, no preinstalled git/curl/python) plus standard macOS/Ubuntu runners; gated by scripts/smoke-test.sh

These two workflows gate development of the tool itself. If you are a consumer who wants CI, see 08 — its example was written for a completely different purpose.

Before opening a PR

bash scripts/audit.sh                        # phải in "AUDIT: clean"
python3 tests/test_hooks.py                  # nếu bạn chạm hooks/
python3 tests/test_indicator_classification.py   # nếu bạn chạm noise_filters/hypothesize (RULE 5)
python3 scripts/backend/intel.py list        # nếu bạn thêm op — nó phải xuất hiện, và audit §2 phải xanh
grep -c '@tool(' intel_engine/harness/tools.py   # nếu bạn thêm @tool — số này phải khớp CLAUDE.md

Everything tracked in this repository is public-facing. Treat it accordingly.