trustsight 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
trustsight/__init__.py ADDED
File without changes
trustsight/analysis.py ADDED
@@ -0,0 +1,178 @@
1
+ import json
2
+
3
+ from .buckets import classify_urls
4
+ from .config import ensure_default_configs, load_config
5
+ from .db import (
6
+ get_last_analysis,
7
+ init_db,
8
+ insert_analysis,
9
+ update_package_version,
10
+ upsert_package,
11
+ )
12
+ from .differ import extract_urls_from_diff, generate_diff
13
+ from .discovery import find_outdated_packages, get_aur_latest_versions, get_installed_aur_packages
14
+ from .fetcher import clone_or_fetch, get_head_commit, get_maintainer_from_commit, get_pkgver_from_head
15
+ from .llm import generate_verdict
16
+ from .novelty import build_novelty_context
17
+ from .rules import apply_rules, get_raw_diff_lines
18
+ from .scoring import calculate_score
19
+ from .llm import fallback_verdict
20
+ from .schema import (
21
+ DiffSummary,
22
+ ExecutionChanges,
23
+ PackageFact,
24
+ fact_to_dict,
25
+ )
26
+ from .tokenizer import tokenize_and_resolve
27
+
28
+
29
+ def analyze_package(pkg_name: str, old_commit: str = "", new_version: str = "") -> PackageFact:
30
+ ensure_default_configs()
31
+ init_db()
32
+ config = load_config()
33
+
34
+ repo = clone_or_fetch(pkg_name)
35
+ head_commit = get_head_commit(repo)
36
+ head_version = get_pkgver_from_head(repo) or new_version
37
+
38
+ if not head_version:
39
+ head_version = new_version
40
+
41
+ package_id = upsert_package(pkg_name, head_version)
42
+
43
+ if not old_commit:
44
+ last = get_last_analysis(package_id)
45
+ if last and last.get("new_commit"):
46
+ old_commit = last["new_commit"]
47
+ else:
48
+ return _make_fresh_analysis(pkg_name, head_version, head_commit, package_id, repo, config)
49
+
50
+ diff_text, diff_summary = generate_diff(repo, old_commit, head_commit, config.get("diff", {}).get("max_context_lines", 3))
51
+ source_changes = extract_urls_from_diff(diff_text)
52
+
53
+ old_maintainer = get_maintainer_from_commit(repo, old_commit) or ""
54
+ new_maintainer = get_maintainer_from_commit(repo, head_commit) or ""
55
+ maintainer_changed = bool(old_maintainer and new_maintainer and old_maintainer != new_maintainer)
56
+
57
+ novelty = build_novelty_context(
58
+ source_changes.added_urls,
59
+ package_id,
60
+ maintainer=new_maintainer,
61
+ )
62
+
63
+ source_buckets = classify_urls(source_changes.added_urls)
64
+
65
+ resolved_strings, unresolved_strings = tokenize_and_resolve(diff_text)
66
+ raw_lines = get_raw_diff_lines(diff_text)
67
+
68
+ triggered_rules = apply_rules(resolved_strings, raw_lines)
69
+ rule_ids = [r["rule_id"] for r in triggered_rules]
70
+
71
+ checksum_map = {
72
+ "changed_from_sha256_to_skip": ("R012", "Checksum changed to SKIP", "HIGH"),
73
+ "checksum_array_emptied": ("R013", "Checksum array emptied", "HIGH"),
74
+ }
75
+ cs_behavior = source_changes.checksum_behavior
76
+ if cs_behavior in checksum_map:
77
+ rid, rname, rsev = checksum_map[cs_behavior]
78
+ triggered_rules.append({
79
+ "rule_id": rid,
80
+ "name": rname,
81
+ "severity": rsev,
82
+ "category": "integrity",
83
+ "match": cs_behavior,
84
+ })
85
+ rule_ids.append(rid)
86
+
87
+ score, breakdown, risk = calculate_score(triggered_rules, source_buckets, novelty, config)
88
+
89
+ fact = PackageFact(
90
+ package_name=pkg_name,
91
+ old_version="",
92
+ new_version=head_version,
93
+ old_commit=old_commit,
94
+ new_commit=head_commit,
95
+ maintainer_changed=maintainer_changed,
96
+ previous_maintainer=old_maintainer,
97
+ current_maintainer=new_maintainer,
98
+ diff_summary=diff_summary,
99
+ source_changes=source_changes,
100
+ source_buckets=source_buckets,
101
+ execution_changes=ExecutionChanges(
102
+ resolved_commands=resolved_strings,
103
+ suspicious_patterns_detected=rule_ids,
104
+ unresolved_patterns=unresolved_strings,
105
+ ),
106
+ novelty_context=novelty,
107
+ score_breakdown=breakdown,
108
+ final_score=score,
109
+ )
110
+
111
+ insert_analysis(
112
+ package_id=package_id,
113
+ old_version="",
114
+ new_version=head_version,
115
+ old_commit=old_commit,
116
+ new_commit=head_commit,
117
+ final_score=score,
118
+ raw_diff=diff_text,
119
+ fact_json=json.dumps(fact_to_dict(fact)),
120
+ triggered_rules=triggered_rules,
121
+ )
122
+
123
+ update_package_version(pkg_name, head_version)
124
+ return fact
125
+
126
+
127
+ def _make_fresh_analysis(
128
+ pkg_name: str, version: str, commit: str, package_id: int, repo, config: dict
129
+ ) -> PackageFact:
130
+ novelty = build_novelty_context([], package_id)
131
+ fact = PackageFact(
132
+ package_name=pkg_name,
133
+ new_version=version,
134
+ new_commit=commit,
135
+ diff_summary=DiffSummary(),
136
+ novelty_context=novelty,
137
+ final_score=0,
138
+ )
139
+ insert_analysis(
140
+ package_id=package_id,
141
+ old_version="",
142
+ new_version=version,
143
+ old_commit="",
144
+ new_commit=commit,
145
+ final_score=0,
146
+ raw_diff="",
147
+ fact_json=json.dumps(fact_to_dict(fact)),
148
+ triggered_rules=[],
149
+ )
150
+ update_package_version(pkg_name, version)
151
+ return fact
152
+
153
+
154
+ def discover_updates(limit: int = 20) -> list[dict]:
155
+ ensure_default_configs()
156
+ init_db()
157
+
158
+ installed = get_installed_aur_packages()
159
+ names = list(installed.keys())
160
+ latest = get_aur_latest_versions(names)
161
+ outdated = find_outdated_packages(installed, latest)
162
+
163
+ results = []
164
+ for name, (old_ver, new_ver) in list(outdated.items())[:limit]:
165
+ fact = analyze_package(name)
166
+ verdict = generate_verdict(fact) if fact.final_score > 0 else fallback_verdict(fact)
167
+ results.append(
168
+ {
169
+ "package": name,
170
+ "old_version": old_ver,
171
+ "new_version": new_ver,
172
+ "score": fact.final_score,
173
+ "verdict": verdict,
174
+ "risk": "Low" if fact.final_score <= 20 else "Medium" if fact.final_score <= 50 else "High" if fact.final_score <= 80 else "Critical",
175
+ }
176
+ )
177
+
178
+ return results
trustsight/buckets.py ADDED
@@ -0,0 +1,59 @@
1
+ import unicodedata
2
+ from urllib.parse import urlparse
3
+
4
+ import tldextract
5
+
6
+ from .config import load_domains
7
+
8
+ CONFUSABLES = {
9
+ "g": "ɡ", "a": "а", "e": "е", "o": "о", "c": "с",
10
+ "p": "р", "x": "х", "y": "у", "i": "і", "l": "ӏ",
11
+ }
12
+
13
+
14
+ def has_homograph(domain: str) -> bool:
15
+ for ch in domain:
16
+ if unicodedata.name(ch, "").startswith("LATIN") and ord(ch) > 127:
17
+ return True
18
+ return False
19
+
20
+
21
+ def classify_url(url: str, domain_config: dict | None = None) -> tuple[str, str]:
22
+ if domain_config is None:
23
+ domain_config = load_domains()
24
+
25
+ parsed = urlparse(url)
26
+ domain = parsed.netloc.lower()
27
+
28
+ if has_homograph(domain):
29
+ return "homograph_attack", domain
30
+
31
+ extracted = tldextract.extract(url)
32
+ registered = f"{extracted.domain}.{extracted.suffix}"
33
+
34
+ raw_hosting = domain_config.get("raw_hosting", {}).get("domains", [])
35
+ for d in raw_hosting:
36
+ if domain == d:
37
+ return "raw_hosting", domain
38
+
39
+ trusted_forges = domain_config.get("trusted_forges", {}).get("domains", [])
40
+ for d in trusted_forges:
41
+ if registered == d or domain.endswith("." + d):
42
+ return "trusted_forge", registered
43
+
44
+ official = domain_config.get("official_projects", {}).get("domains", [])
45
+ for d in official:
46
+ if domain == d or domain.endswith("." + d):
47
+ return "official", domain
48
+
49
+ return "unknown", domain
50
+
51
+
52
+ def classify_urls(
53
+ urls: list[str], domain_config: dict | None = None
54
+ ) -> dict[str, str]:
55
+ result = {}
56
+ for url in urls:
57
+ bucket, matched_domain = classify_url(url, domain_config)
58
+ result[url] = bucket
59
+ return result
trustsight/cli.py ADDED
@@ -0,0 +1,224 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from .analysis import analyze_package, discover_updates
5
+ from .config import ensure_default_configs, load_config, set_config, CONFIG_DIR
6
+ from .db import get_history, get_package_id, get_triggered_rules, init_db
7
+ from .scoring import risk_level
8
+
9
+ RISK_COLORS = {"Low": "green", "Medium": "yellow", "High": "red", "Critical": "bold red"}
10
+
11
+ try:
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+ from rich.text import Text
15
+
16
+ HAS_RICH = True
17
+ except ImportError:
18
+ HAS_RICH = False
19
+
20
+
21
+ def cmd_review(args):
22
+ ensure_default_configs()
23
+ config = load_config()
24
+ limit = args.limit or config.get("limits", {}).get("default_review_limit", 20)
25
+
26
+ results = discover_updates(limit=limit)
27
+
28
+ if not results:
29
+ print("No outdated AUR packages found.")
30
+ return
31
+
32
+ if HAS_RICH:
33
+ console = Console()
34
+ table = Table(title="TrustSight Review")
35
+ table.add_column("Package", style="cyan")
36
+ table.add_column("Score", justify="right")
37
+ table.add_column("Verdict")
38
+
39
+ for r in results:
40
+ score_text = Text(f"{r['score']}/100", style=RISK_COLORS.get(r["risk"], "white"))
41
+ table.add_row(r["package"], score_text, r["verdict"])
42
+
43
+ console.print(table)
44
+ else:
45
+ print(f"{'Package':<20} {'Score':<10} Verdict")
46
+ print("-" * 80)
47
+ for r in results:
48
+ print(f"{r['package']:<20} {r['score']:<10} {r['verdict']}")
49
+
50
+
51
+ def cmd_inspect(args):
52
+ ensure_default_configs()
53
+ init_db()
54
+
55
+ fact = analyze_package(args.package)
56
+
57
+ if HAS_RICH:
58
+ console = Console()
59
+ console.print(f"\n[bold cyan]TrustSight Inspect: {fact.package_name}[/]")
60
+ console.print(f" Version: {fact.old_version} → {fact.new_version}")
61
+ console.print(f" Score: {fact.final_score}/100 [bold {RISK_COLORS.get(risk_level(fact.final_score), 'white')}]({risk_level(fact.final_score)})[/]")
62
+
63
+ if fact.maintainer_changed:
64
+ console.print(f" [yellow]Maintainer changed: {fact.previous_maintainer} → {fact.current_maintainer}[/]")
65
+
66
+ console.print("\n [underline]Diff Summary[/]")
67
+ console.print(f" Files changed: {', '.join(fact.diff_summary.files_changed) or 'none'}")
68
+ console.print(f" Lines: +{fact.diff_summary.lines_added}/-{fact.diff_summary.lines_removed}")
69
+
70
+ cs_behavior = fact.source_changes.checksum_behavior
71
+ if cs_behavior and cs_behavior != "unchanged":
72
+ console.print(f"\n [yellow]Checksum behavior: {cs_behavior}[/]")
73
+
74
+ if fact.source_changes.added_urls:
75
+ console.print("\n [underline]Source URLs Added[/]")
76
+ for url in fact.source_changes.added_urls:
77
+ bucket = fact.source_buckets.get(url, "unknown")
78
+ console.print(f" {url} [dim]({bucket})[/]")
79
+
80
+ if fact.execution_changes.resolved_commands:
81
+ console.print("\n [underline]Resolved Commands[/]")
82
+ for cmd in fact.execution_changes.resolved_commands:
83
+ console.print(f" {cmd}")
84
+
85
+ if fact.score_breakdown:
86
+ console.print("\n [underline]Score Breakdown[/]")
87
+ for entry in fact.score_breakdown:
88
+ sign = "+" if entry.weight >= 0 else ""
89
+ style = RISK_COLORS.get(entry.severity.capitalize(), "white")
90
+ console.print(f" {sign}{entry.weight} [{style}]{entry.severity}[/] {entry.rule_id} {entry.reason[:80]}")
91
+
92
+ console.print("\n [underline]Verdict[/]")
93
+ from .llm import fallback_verdict
94
+ console.print(f" {fallback_verdict(fact)}")
95
+ else:
96
+ print(f"TrustSight Inspect: {fact.package_name}")
97
+ print(f" Version: {fact.old_version} -> {fact.new_version}")
98
+ print(f" Score: {fact.final_score}/100 ({risk_level(fact.final_score)})")
99
+ if fact.source_changes.checksum_behavior and fact.source_changes.checksum_behavior != "unchanged":
100
+ print(f" Checksum: {fact.source_changes.checksum_behavior}")
101
+ if fact.source_changes.added_urls:
102
+ print(" Source URLs Added:")
103
+ for url in fact.source_changes.added_urls:
104
+ bucket = fact.source_buckets.get(url, "unknown")
105
+ print(f" {url} ({bucket})")
106
+
107
+
108
+ def cmd_history(args):
109
+ ensure_default_configs()
110
+ init_db()
111
+
112
+ pkg_id = get_package_id(args.package)
113
+ if pkg_id is None:
114
+ print(f"Package '{args.package}' not found in history.")
115
+ return
116
+
117
+ history = get_history(pkg_id, limit=args.limit or 20)
118
+
119
+ if not history:
120
+ print(f"No analysis history for '{args.package}'.")
121
+ return
122
+
123
+ if HAS_RICH:
124
+ console = Console()
125
+ table = Table(title=f"History: {args.package}")
126
+ table.add_column("Date", style="dim")
127
+ table.add_column("Old", justify="right")
128
+ table.add_column("→ New", justify="right")
129
+ table.add_column("Score", justify="right")
130
+ table.add_column("Risk")
131
+
132
+ for h in history:
133
+ ts = h.get("timestamp", "")[:10] if h.get("timestamp") else ""
134
+ score = h.get("final_score", 0)
135
+ risk = risk_level(score)
136
+ score_text = Text(f"{score}/100", style=RISK_COLORS.get(risk, "white"))
137
+ table.add_row(
138
+ ts,
139
+ h.get("old_version", "") or "",
140
+ h.get("new_version", "") or "",
141
+ score_text,
142
+ risk,
143
+ )
144
+
145
+ console.print(table)
146
+
147
+ if args.score_breakdown and history:
148
+ latest = history[0]
149
+ rules = get_triggered_rules(latest["id"])
150
+ if rules:
151
+ console.print("\n [underline]Latest Score Breakdown[/]")
152
+ for r in rules:
153
+ sign = "+"
154
+ console.print(f" {sign}{r['severity']:<10} {r['rule_id']}")
155
+ else:
156
+ for h in history:
157
+ print(f"{h.get('timestamp','')[:10]:<12} {str(h.get('old_version','')):<12} → {str(h.get('new_version','')):<12} Score: {h.get('final_score',0)}")
158
+
159
+
160
+ def cmd_config(args):
161
+ ensure_default_configs()
162
+ if args.action == "show":
163
+ cfg = load_config()
164
+ print(f"Config file: {CONFIG_DIR / 'config.toml'}")
165
+ print()
166
+ llm = cfg.get("llm", {})
167
+ provider = llm.get("provider", "ollama")
168
+ model = llm.get("model", "gpt-4o-mini")
169
+ print(f" provider: {provider}")
170
+ print(f" model: {model}")
171
+ openai_cfg = llm.get("openai", {})
172
+ api_key = openai_cfg.get("api_key", "")
173
+ base_url = openai_cfg.get("base_url", "https://api.openai.com/v1")
174
+ mask = api_key[:4] + "..." if len(api_key) > 8 else "(not set)"
175
+ print(f" api_key: {mask}")
176
+ print(f" base_url: {base_url}")
177
+ elif args.action == "set":
178
+ if args.key in ("api_key", "base_url"):
179
+ set_config(f"llm.openai.{args.key}", args.value)
180
+ print(f"Set llm.openai.{args.key} in {CONFIG_DIR / 'config.toml'}")
181
+ else:
182
+ print(f"Unknown key: {args.key}. Use api_key or base_url.")
183
+ sys.exit(1)
184
+
185
+
186
+ def main():
187
+ parser = argparse.ArgumentParser(description="TrustSight - AUR Package Update Vetting Tool")
188
+ sub = parser.add_subparsers(dest="command")
189
+
190
+ p_review = sub.add_parser("review", help="Scan AUR and analyze outdated packages")
191
+ p_review.add_argument("--limit", type=int, default=0, help="Max packages to review")
192
+
193
+ p_inspect = sub.add_parser("inspect", help="Analyze a specific package")
194
+ p_inspect.add_argument("package", help="Package name")
195
+
196
+ p_history = sub.add_parser("history", help="Show analysis history for a package")
197
+ p_history.add_argument("package", help="Package name")
198
+ p_history.add_argument("--limit", type=int, default=20, help="Max history entries")
199
+ p_history.add_argument("--score-breakdown", action="store_true", help="Show score breakdown")
200
+
201
+ p_config = sub.add_parser("config", help="Manage configuration")
202
+ p_config_sub = p_config.add_subparsers(dest="action")
203
+
204
+ p_config_sub.add_parser("show", help="Show current configuration")
205
+ p_config_set = p_config_sub.add_parser("set", help="Set a configuration value")
206
+ p_config_set.add_argument("key", choices=["api_key", "base_url"], help="Config key")
207
+ p_config_set.add_argument("value", help="Config value")
208
+
209
+ args = parser.parse_args()
210
+
211
+ if args.command == "review":
212
+ cmd_review(args)
213
+ elif args.command == "inspect":
214
+ cmd_inspect(args)
215
+ elif args.command == "history":
216
+ cmd_history(args)
217
+ elif args.command == "config":
218
+ cmd_config(args)
219
+ else:
220
+ parser.print_help()
221
+
222
+
223
+ if __name__ == "__main__":
224
+ main()