feat(creds): cred-reuse foundation + vectorstore scaffold
Lays the storage and bus substrate for the "credential reuse patterns"
task in DEVELOPMENT.md and scaffolds decnet/vectorstore/ as the future
substrate for statistical attacker re-identification over behavioral
fingerprints. No correlator, profiler, API, or dashboard wiring in
this commit — see TODO.md for the handoff.
Schema:
- Credential.attacker_uuid (nullable FK to attackers.uuid),
backfilled by the profiler post-write to avoid coupling the
capture path to the profiler's ordering.
- CredentialReuse table — UUID PK, JSON list columns for the
accumulating attacker_uuids/ips/deckies/services, target_count
(the discriminative scalar), confidence reserved for a future
fuzzy-credential pass.
Repo:
- upsert_credential_reuse / list_credential_reuses /
get_credential_reuse_by_id / update_credential_attacker_uuid.
- Renamed pre-existing get_credential_reuse(secret_sha256) to
get_credential_attempts_for_secret(secret_sha256) — the new
findings table needs the cleaner name.
Bus topics:
- credential.captured (one per Credential upsert)
- credential.reuse.detected (correlator-emitted on insert/grow)
Vectorstore subpackage (decnet/vectorstore/, flat layout mirroring
decnet/bus/):
- BaseVectorStore ABC keyed by (kind, id) — kind discriminator
means new feature families are additive, no schema migration.
- FakeVectorStore (in-memory L2 KNN), NullVectorStore (no-op for
DECNET_VECTORSTORE_ENABLED=false), SqliteVecVectorStore (lazy
sqlite_vec extension load, one vec0 virtual table per kind).
- get_vectorstore() env-driven dispatch with graceful fallback
to FakeVectorStore when the sqlite-vec extension isn't on the
host, so workers don't crash on a missing optional dep.
Tests: 26 new (11 cred-reuse repo, 15 vectorstore). Existing
credentials and base-repo tests updated for the rename. Total: 34
passing on the touched files.
This commit is contained in:
@@ -23,7 +23,11 @@ class DummyRepo(BaseRepository):
|
||||
async def get_credentials(self, **kw): await super().get_credentials(**kw)
|
||||
async def get_total_credentials(self, **kw): await super().get_total_credentials(**kw)
|
||||
async def get_credentials_for_attacker(self, ip): await super().get_credentials_for_attacker(ip)
|
||||
async def get_credential_reuse(self, h): await super().get_credential_reuse(h)
|
||||
async def get_credential_attempts_for_secret(self, h): await super().get_credential_attempts_for_secret(h)
|
||||
async def upsert_credential_reuse(self, **kw): await super().upsert_credential_reuse(**kw); return None
|
||||
async def list_credential_reuses(self, **kw): await super().list_credential_reuses(**kw); return (0, [])
|
||||
async def get_credential_reuse_by_id(self, i): await super().get_credential_reuse_by_id(i)
|
||||
async def update_credential_attacker_uuid(self, ip, u): await super().update_credential_attacker_uuid(ip, u); return 0
|
||||
async def get_state(self, k): await super().get_state(k)
|
||||
async def set_state(self, k, v): await super().set_state(k, v)
|
||||
async def get_max_log_id(self): await super().get_max_log_id()
|
||||
@@ -73,7 +77,15 @@ async def test_base_repo_coverage():
|
||||
await dr.get_credentials()
|
||||
await dr.get_total_credentials()
|
||||
await dr.get_credentials_for_attacker("1.2.3.4")
|
||||
await dr.get_credential_reuse("abc")
|
||||
await dr.get_credential_attempts_for_secret("abc")
|
||||
await dr.upsert_credential_reuse(
|
||||
secret_sha256="x", secret_kind="plaintext", principal=None,
|
||||
attacker_uuid=None, attacker_ip="1.2.3.4", decky="d", service="ssh",
|
||||
attempt_count=1, ts=None,
|
||||
)
|
||||
await dr.list_credential_reuses()
|
||||
await dr.get_credential_reuse_by_id("a")
|
||||
await dr.update_credential_attacker_uuid("1.2.3.4", "u")
|
||||
await dr.get_state("k")
|
||||
await dr.set_state("k", "v")
|
||||
await dr.get_max_log_id()
|
||||
|
||||
Reference in New Issue
Block a user