Relationships store types#1473
Conversation
…tering ## Problem Bundle export API filters relationships by entity type using wildcard queries on source_ref/target_ref URIs (e.g., `source_ref:*malware*`). This is inefficient as it doesn't use the ES inverted index. ## Solution - Add explicit `source_type` and `target_type` fields to relationship docs - Extract type from URI on write (e.g., `malware` from `.../ctia/malware/...`) - Query uses backward-compatible OR clause during migration: `((source_type:malware) OR (source_ref:*malware*))` ## Migration Strategy (Zero-Downtime) 1. Deploy this PR → new docs get fast path, old docs use wildcard fallback 2. Run ES update_by_query to populate fields on existing docs 3. Remove wildcard fallback in follow-up PR ## Files - src/ctia/entity/relationship/es_store.clj - New ES store with mapping & transforms - src/ctia/bundle/core.clj - Backward-compatible query generation - src/ctia/entity/relationship.clj - Use new es-store - src/ctia/task/check_es_stores.clj - Update schema reference Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
f8145d0 to
95901e0
Compare
…gr6) Adds a bash script that uses Elasticsearch's _update_by_query API with a Painless script to backfill source_type and target_type fields on existing relationship documents. Usage: # Dry run ./scripts/migrate_relationship_types.sh localhost:9200 ctia_relationship --dry-run # Execute ./scripts/migrate_relationship_types.sh localhost:9200 ctia_relationship The script extracts entity type from source_ref/target_ref URLs: http://example.com/ctia/malware/malware-123 -> source_type: "malware" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
bbee7f9 to
e158c27
Compare
Tests for: - stored-relationship->es-stored-relationship (extracts source_type/target_type) - es-stored-relationship->stored-relationship (removes type fields) - es-partial-stored-relationship->partial-stored-relationship - store-opts functions with :doc wrapper Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
New options: - --throttle <N>: Limit to N documents per second using requests_per_second - --async: Run in background with wait_for_completion=false, returns task ID Recommended for production: ./scripts/migrate_relationship_types.sh host:9200 index --throttle 500 --async Also includes: - Estimated time calculation in dry-run mode - Task monitoring and cancellation commands for async mode Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use heredoc with temp file to avoid shell escaping issues with Painless script - The forward slashes in '/ctia/' were being misinterpreted - Tested and verified working against OpenSearch 2.19 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds check_relationship_migration.sh that: - Shows migration statistics (total, migrated, remaining) - Displays progress percentage - Shows breakdown by source_type and target_type - Validates sample documents (ensures extracted types match refs) - Returns exit codes: 0=success, 1=validation error, 2=incomplete Usage: ./scripts/check_relationship_migration.sh <ES_HOST> <INDEX> Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use cond-> to only assoc source_type/target_type when non-nil - Update test data to use valid CTIA ID format (type-uuid pattern) - Add test case for graceful handling of unparseable refs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
yogsototh
left a comment
There was a problem hiding this comment.
Verdict: NEEDS DISCUSSION (comment-only) -- 0 CRITICAL, 1 HIGH (latent), 4 MEDIUM. Current delivered behavior is correct and tested; the HIGH is a landmine for the planned follow-up, not a runtime break in this PR.
Design is sound: the new query is a strict superset of master (((source_type:X) OR (source_ref:*X*))), so there is no wrong-results window during the staged migration, and the read paths strip source_type/target_type so API responses are unchanged. Verified: the moved RelationshipStore/relationship-mapping symbols have no external consumers, the node-filters arity change has no stale caller, and the check_es_stores.clj override to ESStoredRelationship is required (real migrated docs would fail strict coercion against plain StoredRelationship).
Findings
-
HIGH [correctness] -- Hyphenated types break the exact-match clause once the wildcard fallback is removed:
node-filtersemitssource_type:attack-pattern(unquoted) into a rawquery_string. In the classic parser the-is the prohibit operator, so for every hyphenated CTIA type (attack-pattern,data-table,identity-assertion, ...) the new exact-match term does not match the keyword token. Today this is masked by theOR (source_ref:*attack-pattern*)fallback (current results are correct), but the PR's stated Phase 6 removes that fallback -- at which point hyphenated-type filtering silently returns empty/wrong results. No test covers a hyphenated type (core_test.cljonly usesmalware/sighting/vulnerability/incident). Fix before the follow-up: quote/escape the term (source_type:"attack-pattern") or use a structured term filter for the exact clause; add a hyphenated-type test. (src/ctia/bundle/core.clj:422) -
MEDIUM [correctness/ops] -- Migration-completeness gate can never reach 0: any relationship whose ref lacks a parseable
/ctia/<type>/segment (legacy/imported/malformed) never gets the field set, so it permanently matches the "needs migration" query andcheck_relationship_migration.shexits2forever. Consider restricting the needs-migration query to docs whose refs match*/ctia/*, or marking unextractable docs as excluded. (scripts/check_relationship_migration.sh:51) -
MEDIUM [security] -- ES credentials exposed (cleartext HTTP + process table): both scripts hardcode
http://${ES_HOST}while passing-u ${ES_USER}:${ES_PASS}on the curl argv (visible inps//proc/<pid>/cmdline), and the docs steer operators at prod hosts. Use--netrc-file/ a bash array, and make the scheme configurable (ES_SCHEME/detecthttps://). (scripts/migrate_relationship_types.sh:79) -
MEDIUM [test/quality] -- Loose return annotation defeats the schema fixture:
es-stored-relationship->stored-relationshipis annotated:- ESStoredRelationship(optional type keys) but exists to strip those keys and return aStoredRelationship. A regression that failed to dissoc would still validate, sovalidate-schemasgives zero protection for the exact bug this fn prevents. Tighten the annotation tors/StoredRelationship. (src/ctia/entity/relationship/es_store.clj:51) -
MEDIUM [cross-cutting] -- Three divergent "extract type from ref" implementations, none cross-tested: Clojure
long-id->id(strict regex, returns nil on malformed), PainlessindexOf('/ctia/')+substring(no validation), andsedin the checker all differ on malformed refs (e.g.…/ctia/malware/not-a-uuid-> nil in Clojure,malwarein Painless/sed). Acceptable for one-shot tooling, but document the intentional difference or add a divergence test. (scripts/migrate_relationship_types.sh:172)
PASS: Security (no injection -- entity types are enum-validated; Painless is pure string ops, no eval), Cross-cutting contracts (no moved-symbol/arity break), Deployment ordering (no wrong-results window).
Also left 7 inline comments.
Non-blocking follow-ups (LOW)
- Remove unused require
ctia.entity.event.schemas :as es(src/ctia/entity/relationship.clj:19). - Remove dead defs
ESPartialStoredRelationshipList/PartialStoredRelationshipList(src/ctia/entity/relationship/es_store.clj:37-38). grep -o ... || echo 0swallows curl/network/auth failures and can misreport a failed_update_by_queryas a no-op success (scripts/migrate_relationship_types.sh:108,233); check curl exit code + HTTP status.- Add a behavioral test for the legacy wildcard fallback: insert a relationship doc WITHOUT the type fields and assert bundle export by
source_typestill returns it viasource_ref:*X*.core_test.cljasserts only the query string. - Add an assertion in
bundle-export-graph-testthat exported relationships omitsource_type/target_type.
-- auto-review-team (BOT_APPROVABLE gate)
| [new-field old-field entity-types] | ||
| (let [type-names (map name entity-types) | ||
| new-field-query (->> type-names | ||
| (map #(format "%s:%s" new-field %)) |
There was a problem hiding this comment.
HIGH -- Hyphenated types break the exact-match clause once the wildcard fallback is removed: this emits source_type:attack-pattern (unquoted) into a raw query_string. In the classic Lucene parser an unescaped - is the prohibit operator, so for every hyphenated CTIA type (attack-pattern, data-table, identity-assertion, ...) the exact term does not match the keyword token. Today the OR (source_ref:*attack-pattern*) fallback masks this (results are correct), but Phase 6 of the deployment plan removes that fallback -- after which hyphenated-type filtering silently returns empty/wrong results. Suggest quoting the term (source_type:"attack-pattern") or using a structured term filter, and adding a hyphenated-type case to core_test.clj.
| target-type (assoc :target_type target-type)))) | ||
|
|
||
| (s/defn es-stored-relationship->stored-relationship | ||
| :- ESStoredRelationship |
There was a problem hiding this comment.
MEDIUM -- Return annotation defeats the schema fixture: this fn exists to strip :source_type/:target_type and return a StoredRelationship, but it is annotated :- ESStoredRelationship (which has those keys as optional). A regression that failed to dissoc would still validate against ESStoredRelationship, so the :once validate-schemas fixture gives zero protection for the exact bug this fn prevents. The mirrored sighting transform (es-stored-sighting->stored-sighting) correctly returns the narrowed schema. Suggest :- rs/StoredRelationship.
| rs/StoredRelationship | ||
| rs/PartialStoredRelationship) | ||
| [schema.core :as s] | ||
| [ctia.entity.event.schemas :as es])) |
There was a problem hiding this comment.
LOW -- Unused require: the es alias for ctia.entity.event.schemas is never referenced in this namespace. Remove it to avoid an unnecessary dependency edge and linter noise.
| {:source_type s/Str | ||
| :target_type s/Str}))) | ||
|
|
||
| (def ESPartialStoredRelationshipList (list-response-schema ESPartialStoredRelationship)) |
There was a problem hiding this comment.
LOW -- Dead code: ESPartialStoredRelationshipList (and PartialStoredRelationshipList on the next line) are defined but referenced nowhere in src/ or test/. Remove, or wire them into the route/swagger schemas if they were intended for a response type.
| -d '{"query":{"bool":{"must":[{"term":{"type":"relationship"}},{"exists":{"field":"source_type"}},{"exists":{"field":"target_type"}}]}}}' | grep -o '"count":[0-9]*' | grep -o '[0-9]*' || echo "0") | ||
|
|
||
| # Documents missing EITHER field (need migration) | ||
| NEEDS_MIGRATION=$(curl -s ${AUTH_OPTS} "http://${ES_HOST}/${INDEX_NAME}/_count" \ |
There was a problem hiding this comment.
MEDIUM -- Completeness gate can never reach 0: any relationship whose source_ref/target_ref lacks a parseable /ctia/<type>/ segment (legacy/imported/malformed) never gets the field populated by the Painless migration, so it permanently matches this "needs migration" query and the script exits 2 (line 181) forever. Consider restricting this query to docs whose refs match */ctia/*, or otherwise excluding unextractable docs so the migration can be declared complete.
| # Build auth header if credentials provided | ||
| AUTH_OPTS="" | ||
| if [[ -n "${ES_USER:-}" && -n "${ES_PASS:-}" ]]; then | ||
| AUTH_OPTS="-u ${ES_USER}:${ES_PASS}" |
There was a problem hiding this comment.
MEDIUM -- Credential exposure: -u ${ES_USER}:${ES_PASS} is expanded onto the curl argv (lines 91/125/195), so the cleartext user:password is visible in ps -ef / /proc/<pid>/cmdline to any local user; combined with the hardcoded http:// scheme (and the prod-host example in the sibling script), the secret is also exposed on the wire. Suggest curl --netrc-file (or a bash array AUTH_OPTS=(-u "$ES_USER:$ES_PASS")) and a configurable ES_SCHEME/https:// detection.
| } | ||
| }') | ||
|
|
||
| DOC_COUNT=$(echo "${COUNT_RESPONSE}" | grep -o '"count":[0-9]*' | grep -o '[0-9]*' || echo "0") |
There was a problem hiding this comment.
LOW -- || echo 0 masks curl/network/auth failures: if curl fails entirely (network error, auth rejected, ES error JSON), this swallows it and reports DOC_COUNT=0 -> "No documents need migration. Done." The same idiom at line 233 (UPDATED) can misreport a failed _update_by_query as a no-op success. Suggest checking curl's exit code and the HTTP status separately before parsing the count.
Summary
Add
source_typeandtarget_typefields to relationship documents for efficient filtering in bundle/export API.Problem
source_type/target_typeparamssource_ref:*malware*(wildcard = inefficient, doesn't use ES index)source_type:malware(exact match = uses ES index efficiently)Solution
source_type/target_typefields extracted from URIs on write((source_type:X) OR (source_ref:*X*))_update_by_queryto backfill existing documentsFiles Changed
src/ctia/entity/relationship/es_store.clj- ES mapping and transformssrc/ctia/bundle/core.clj- Backward-compatible query generationsrc/ctia/entity/relationship.clj- Use new es-storescripts/migrate_relationship_types.sh- ES migration scriptscripts/check_relationship_migration.sh- Migration verification scripttest/ctia/entity/relationship/es_store_test.clj- Unit tests for transformstest/ctia/bundle/core_test.clj- Updated query testsDeployment Steps
Phase 1: Deploy this PR
New documents will automatically get
source_type/target_typepopulated.Queries use backward-compatible OR clause (works for both new and old docs).
Phase 2: Update ES mapping
Add the new field definitions to the existing index:
Phase 3: Run migration script
Backfill existing documents with
source_type/target_typevalues:Phase 4: Verify migration
Check migration status and validate data:
Output includes:
Phase 5: Refresh mappings
Reindex documents so new fields are properly searchable:
Phase 6: Follow-up PR
Once migration is complete in all environments, deploy follow-up PR to remove the wildcard fallback from queries.
Script Options
migrate_relationship_types.sh
--dry-run--throttle <N>--asynccheck_relationship_migration.sh
QA Checklist
source_type/target_typepopulatedsource_typefilter works for both new and old docs--dry-runfirst--throttle 500 --asynccheck_relationship_migration.shto verifyRelated
🤖 Generated with Claude Code