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 +0 -0
- trustsight/analysis.py +178 -0
- trustsight/buckets.py +59 -0
- trustsight/cli.py +224 -0
- trustsight/config.py +281 -0
- trustsight/db.py +179 -0
- trustsight/differ.py +85 -0
- trustsight/discovery.py +58 -0
- trustsight/fetcher.py +84 -0
- trustsight/llm.py +179 -0
- trustsight/novelty.py +78 -0
- trustsight/rules.py +47 -0
- trustsight/schema.py +102 -0
- trustsight/scoring.py +108 -0
- trustsight/tokenizer.py +60 -0
- trustsight-0.1.0.dist-info/METADATA +295 -0
- trustsight-0.1.0.dist-info/RECORD +19 -0
- trustsight-0.1.0.dist-info/WHEEL +4 -0
- trustsight-0.1.0.dist-info/entry_points.txt +2 -0
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()
|