Skip to content

Commit 2bf3a08

Browse files
feat: add signal override evidence generation and update related components
1 parent f35e4c5 commit 2bf3a08

File tree

5 files changed

+74
-3
lines changed

5 files changed

+74
-3
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
**This tool is for lawful, ethical OSINT research only.**
88
Do not use it to harass, stalk, dox, or violate privacy. Always comply with applicable laws and third-party Terms of Service.
99

10+
11+
!()[https://imgur.com/XCNwZsf.png]
12+
!()[https://imgur.com/uHusxTO.png]
13+
14+
1015
## Table of Contents
1116

1217
- What This Tool Does
@@ -163,6 +168,8 @@ Example:
163168
}
164169
```
165170

171+
When an override fires, a synthetic evidence item with `source: signal_override` is appended to the report so the Evidence list (CLI/GUI/CSV) records why the signal changed even if no adapter returned data.
172+
166173
You can also set a custom path via `PHONEINT_SIGNAL_OVERRIDES_PATH`.
167174

168175
## Configuration

phoneint/cli.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@
3131
from phoneint.reputation.google import GoogleCustomSearchAdapter
3232
from phoneint.reputation.public import PublicScamListAdapter
3333
from phoneint.reputation.score import infer_domain_signals, score_risk
34-
from phoneint.reputation.signals import apply_signal_overrides, load_signal_overrides
34+
from phoneint.reputation.signals import (
35+
apply_signal_overrides,
36+
generate_signal_override_evidence,
37+
load_signal_overrides,
38+
)
3539

3640
logger = logging.getLogger(__name__)
3741

@@ -134,6 +138,9 @@ async def lookup_async(
134138
domain_signals=domain_signals,
135139
overrides=signal_overrides,
136140
)
141+
override_evidence = generate_signal_override_evidence(normalized.e164, override_hits)
142+
if override_evidence:
143+
evidence.extend(override_evidence)
137144

138145
score = score_risk(
139146
found_in_scam_db=found_in_scam_db,

phoneint/gui.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@
2929
from phoneint.reputation.google import GoogleCustomSearchAdapter
3030
from phoneint.reputation.public import PublicScamListAdapter
3131
from phoneint.reputation.score import infer_domain_signals, score_risk
32-
from phoneint.reputation.signals import apply_signal_overrides, load_signal_overrides
32+
from phoneint.reputation.signals import (
33+
apply_signal_overrides,
34+
generate_signal_override_evidence,
35+
load_signal_overrides,
36+
)
3337

3438
logger = logging.getLogger(__name__)
3539

@@ -450,6 +454,11 @@ async def run_one(adapter: ReputationAdapter) -> tuple[str, list[SearchResult]]:
450454
domain_signals=domain_signals,
451455
overrides=self._signal_overrides,
452456
)
457+
override_evidence = generate_signal_override_evidence(normalized.e164, override_hits)
458+
if override_evidence:
459+
evidence.extend(override_evidence)
460+
for entry in override_evidence:
461+
self.evidence_list.addItem(f"[{entry.source}] {entry.title}")
453462
score = score_risk(
454463
found_in_scam_db=found_in_scam_db,
455464
voip=voip,

phoneint/reputation/signals.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import json
44
from pathlib import Path
55

6+
from phoneint.reputation.adapter import SearchResult, now_utc
7+
68
SIGNAL_NAMES = (
79
"voip",
810
"found_in_classifieds",
@@ -11,6 +13,12 @@
1113

1214
DEFAULT_SIGNAL_OVERRIDES_PATH = Path("phoneint/data/signal_overrides.json")
1315

16+
SIGNAL_DISPLAY_NAMES = {
17+
"voip": "VoIP signal",
18+
"found_in_classifieds": "classifieds mention",
19+
"business_listing": "business directory mention",
20+
}
21+
1422

1523
def _normalize_number(value: str) -> str:
1624
return value.strip()
@@ -65,3 +73,25 @@ def apply_signal_overrides(
6573
hits[name] = True
6674

6775
return voip_signal, merged, hits
76+
77+
78+
def generate_signal_override_evidence(
79+
e164: str, hits: dict[str, bool]
80+
) -> list[SearchResult]:
81+
"""Return synthetic evidence entries for any fired signal overrides."""
82+
83+
entries: list[SearchResult] = []
84+
for name in SIGNAL_NAMES:
85+
if not hits.get(name):
86+
continue
87+
label = SIGNAL_DISPLAY_NAMES.get(name, name)
88+
entries.append(
89+
SearchResult(
90+
title=f"Signal override: {label}",
91+
url="",
92+
snippet=f"Configured override flagged the {label} for {e164}.",
93+
timestamp=now_utc(),
94+
source="signal_override",
95+
)
96+
)
97+
return entries

tests/test_signals.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
from pathlib import Path
44
import json
55

6-
from phoneint.reputation.signals import SIGNAL_NAMES, apply_signal_overrides, load_signal_overrides
6+
from phoneint.reputation.signals import (
7+
SIGNAL_NAMES,
8+
apply_signal_overrides,
9+
generate_signal_override_evidence,
10+
load_signal_overrides,
11+
)
712

813

914
def test_load_signal_overrides_missing(tmp_path: Path) -> None:
@@ -47,3 +52,16 @@ def test_apply_signal_overrides_forces_flags() -> None:
4752
assert hits["voip"] is True
4853
assert hits["found_in_classifieds"] is True
4954
assert hits["business_listing"] is False
55+
56+
57+
def test_generate_signal_override_evidence_entries() -> None:
58+
hits = {
59+
"voip": True,
60+
"found_in_classifieds": False,
61+
"business_listing": True,
62+
}
63+
entries = generate_signal_override_evidence("+14155552671", hits)
64+
assert len(entries) == 2
65+
assert entries[0].source == "signal_override"
66+
assert "Signal override" in entries[0].title
67+
assert entries[1].source == "signal_override"

0 commit comments

Comments
 (0)