Skip to content

Commit 69e9b41

Browse files
first commit
0 parents  commit 69e9b41

37 files changed

Lines changed: 1489 additions & 0 deletions

.github/FUNDING.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github: programmersd21s

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
__pycache__/
2+
*.pyc
3+
.benchmarks/
4+
.pytest_cache/
5+
.mypy_cache/
6+
.ruff_cache/
7+
dist/
8+
build/
9+
*.egg-info/

LICENSE.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
MIT License
2+
3+
Copyright (c) 2026 programmersd21
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
22+
23+
Contact: geniussantu1983@gmail.com

README.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<div align="center">
2+
3+
# aur_checker
4+
5+
A CLI tool to inspect Arch Linux AUR PKGBUILD files for security risks using static analysis and AI.
6+
7+
<p align="center">
8+
<img src="https://img.shields.io/github/actions/workflow/status/programmersd21/aur_checker/ci.yml?style=for-the-badge" alt="Build Status">
9+
<img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge" alt="License Badge">
10+
<img src="https://img.shields.io/badge/Python-3.10+-blue?style=for-the-badge&logo=python" alt="Python Version">
11+
</p>
12+
13+
> **Support this project** - [Sponsor](https://github.com/sponsors/programmersd21) or star the repo.
14+
15+
---
16+
17+
## Pipeline
18+
19+
```
20+
PKGBUILD → FETCH → STATIC_ANALYSIS → METADATA → SCORING → AI (70% weight) → VERDICT
21+
```
22+
23+
1. **Fetch** - Downloads PKGBUILD from AUR CGit.
24+
2. **Static analysis** - Regex scan for remote execution, obfuscation, system modification, package manager calls.
25+
3. **Metadata** - AUR RPC v5 info (maintainer, orphan status, age).
26+
4. **Scoring** - Aggregates signals into a 0-100 risk score.
27+
5. **AI** - `gemini-3.1-flash-lite` via `google-genai`. Blended 30/70 (static/AI) for the final verdict.
28+
6. **Output** - Rich terminal output or JSON.
29+
30+
---
31+
32+
## Installation
33+
34+
```bash
35+
git clone https://github.com/programmersd21/aur_checker.git
36+
cd aur_checker
37+
pip install -e .
38+
```
39+
40+
---
41+
42+
## AI Configuration
43+
44+
AI runs automatically. Set your API key:
45+
46+
| Platform | Command |
47+
|----------|---------|
48+
| Linux/macOS | `export AURCHECKER_AI_API_KEY="your-key"` |
49+
| Windows PowerShell | `$env:AURCHECKER_AI_API_KEY="your-key"` |
50+
51+
Optional overrides:
52+
- `AURCHECKER_AI_MODEL` - model name (default: `gemini-3.1-flash-lite`)
53+
- `AURCHECKER_AI_TIMEOUT` - timeout in ms (default: `120000`)
54+
55+
---
56+
57+
## Usage
58+
59+
```bash
60+
# Check a single package
61+
aur_checker check keepassx2
62+
63+
# JSON output
64+
aur_checker --json check keepassx2
65+
66+
# Check multiple packages
67+
aur_checker batch keepassx2 visual-studio-code-bin
68+
aur_checker batch --file packages.txt
69+
70+
# AI analysis from saved JSON
71+
aur_checker explain --input analysis.json
72+
73+
# Check + install (makepkg required)
74+
aur_checker install keepassx2
75+
76+
# Clear cached results
77+
aur_checker clear-cache
78+
```
79+
80+
---
81+
82+
## Scoring
83+
84+
| Score | Level | Verdict |
85+
|-------|-------|---------|
86+
| 0-20 | LOW | ALLOW |
87+
| 21-50 | MEDIUM | REVIEW |
88+
| 51-100 | HIGH | DENY |
89+
90+
Signals detected by static analysis:
91+
92+
| Signal | Trigger | Max weight |
93+
|--------|---------|-----------|
94+
| remote_exec | curl/wget piped to shell | 50 |
95+
| external_calls | Non-source HTTPS URLs | 15 |
96+
| pkg_manager | npm/pip/cargo/go/gem/pacman/yay | 10 |
97+
| orphan_adopted | Package without maintainer | 10 |
98+
| obfuscation | base64, hex, printf+xxd, openssl, eval | 30 |
99+
| system_mod | Writing to /etc, /usr/lib, /opt, /boot | 30 |
100+
| maintainer_changed | Maintainer turnover (always UNKNOWN) | 0 |
101+
102+
---
103+
104+
## Quality Assurance
105+
106+
```bash
107+
pytest
108+
mypy .
109+
ruff check .
110+
```
111+
112+
---
113+
114+
## License
115+
116+
MIT. See [LICENSE.md](LICENSE.md).
117+
118+
Contact: [geniussantu1983@gmail.com](mailto:geniussantu1983@gmail.com)
119+
120+
</div>

__main__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from cli.root import app
2+
3+
if __name__ == "__main__":
4+
app()

ai/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Empty init file for ai package

ai/client.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import json
2+
import os
3+
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout
4+
from typing import Any
5+
from google import genai
6+
from google.genai import types
7+
from core.context import PipelineContext, AIOutput, ErrorDetail
8+
from ai.contract import AIOutputContract
9+
10+
11+
def _call_gemini(
12+
api_key: str,
13+
model_name: str,
14+
prompt: str,
15+
timeout_ms: int,
16+
) -> dict[str, Any]:
17+
client = genai.Client(
18+
api_key=api_key,
19+
http_options=types.HttpOptions(timeout=timeout_ms),
20+
)
21+
22+
contents = [
23+
types.Content(
24+
role="user",
25+
parts=[
26+
types.Part.from_text(text=prompt),
27+
],
28+
),
29+
]
30+
31+
gen_config = types.GenerateContentConfig()
32+
33+
response = client.models.generate_content(
34+
model=model_name,
35+
contents=contents, # type: ignore[arg-type]
36+
config=gen_config,
37+
)
38+
39+
text = ""
40+
if response.text:
41+
text = response.text.strip()
42+
43+
if not text:
44+
raise ValueError("AI returned an empty response.")
45+
46+
result: dict[str, Any] = json.loads(text)
47+
return result
48+
49+
50+
def analyze_with_gemini(ctx: PipelineContext) -> PipelineContext:
51+
api_key = os.getenv("AURCHECKER_AI_API_KEY")
52+
if not api_key:
53+
ctx.errors.append(
54+
ErrorDetail(
55+
code="AI_DISABLED", stage="AI", message="AURCHECKER_AI_API_KEY env var is not set.", recoverable=True
56+
)
57+
)
58+
return ctx
59+
60+
if not ctx.features or not ctx.metadata:
61+
ctx.errors.append(
62+
ErrorDetail(
63+
code="AI_SCHEMA_INVALID",
64+
stage="AI",
65+
message="Cannot run AI analysis without features and metadata.",
66+
recoverable=True,
67+
)
68+
)
69+
return ctx
70+
71+
payload = {
72+
"package": ctx.package,
73+
"features": ctx.features.dict(),
74+
"metadata": ctx.metadata.dict(),
75+
"risk_score": ctx.risk_score,
76+
"risk_level": ctx.risk_level,
77+
}
78+
payload_str = json.dumps(payload)
79+
80+
model_name = os.getenv("AURCHECKER_AI_MODEL", "gemini-3.1-flash-lite")
81+
timeout_ms = int(os.getenv("AURCHECKER_AI_TIMEOUT", "120000"))
82+
deadline_s = timeout_ms / 1000 + 5
83+
84+
prompt = f"""You are a security expert analyzing an Arch AUR PKGBUILD. Output ONLY valid JSON matching this schema exactly:
85+
{{
86+
"summary": "string (max 200 chars) - brief risk overview",
87+
"suspicious_patterns": ["string"] - list of specific dangerous patterns found (max 5)",
88+
"attack_surface": "string (max 300 chars) - what an attacker could exploit",
89+
"rationale": ["string"] - reasons for the risk score (max 3)",
90+
"ai_risk_score": integer 0-100 - your independent risk score based on PKGBUILD content",
91+
"ai_risk_level": "LOW" or "MEDIUM" or "HIGH",
92+
"ai_risk_details": "string (max 500 chars) - what specifically contributed to the risk score"
93+
}}
94+
95+
Input: {payload_str}
96+
97+
Output valid JSON only. No markdown. No prose."""
98+
99+
try:
100+
with ThreadPoolExecutor(max_workers=1) as pool:
101+
fut = pool.submit(_call_gemini, api_key, model_name, prompt, timeout_ms)
102+
result = fut.result(timeout=deadline_s)
103+
104+
validated = AIOutputContract(**result)
105+
106+
ctx.ai_analysis = AIOutput(
107+
summary=validated.summary,
108+
suspicious_patterns=validated.suspicious_patterns,
109+
attack_surface=validated.attack_surface,
110+
rationale=validated.rationale,
111+
ai_risk_score=validated.ai_risk_score,
112+
ai_risk_level=validated.ai_risk_level,
113+
ai_risk_details=validated.ai_risk_details,
114+
)
115+
except FuturesTimeout:
116+
ctx.errors.append(
117+
ErrorDetail(
118+
code="AI_TIMEOUT",
119+
stage="AI",
120+
message=f"AI did not respond within {int(deadline_s)}s.",
121+
recoverable=True,
122+
)
123+
)
124+
except json.JSONDecodeError as e:
125+
ctx.errors.append(
126+
ErrorDetail(code="AI_SCHEMA_INVALID", stage="AI", message=f"JSON Decode Error: {str(e)}", recoverable=True)
127+
)
128+
except Exception as e:
129+
ctx.errors.append(
130+
ErrorDetail(
131+
code="AI_TIMEOUT" if "timeout" in str(e).lower() else "AI_SCHEMA_INVALID",
132+
stage="AI",
133+
message=str(e),
134+
recoverable=True,
135+
)
136+
)
137+
138+
return ctx

ai/contract.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from pydantic import BaseModel, field_validator, Field
2+
from typing import List
3+
4+
5+
class AIOutputContract(BaseModel):
6+
summary: str
7+
suspicious_patterns: List[str]
8+
attack_surface: str
9+
rationale: List[str]
10+
ai_risk_score: int = Field(default=0, ge=0, le=100)
11+
ai_risk_level: str = Field(default="LOW")
12+
ai_risk_details: str = ""
13+
14+
@field_validator("summary")
15+
@classmethod
16+
def validate_summary(cls, v):
17+
return str(v)[:200]
18+
19+
@field_validator("suspicious_patterns")
20+
@classmethod
21+
def validate_patterns(cls, v):
22+
return [str(p) for p in v][:5]
23+
24+
@field_validator("attack_surface")
25+
@classmethod
26+
def validate_attack_surface(cls, v):
27+
return str(v)[:300]
28+
29+
@field_validator("rationale")
30+
@classmethod
31+
def validate_rationale(cls, v):
32+
return [str(r) for r in v][:3]
33+
34+
@field_validator("ai_risk_level")
35+
@classmethod
36+
def validate_risk_level(cls, v):
37+
v = str(v).upper()
38+
if v not in ("LOW", "MEDIUM", "HIGH"):
39+
return "LOW"
40+
return v
41+
42+
@field_validator("ai_risk_details")
43+
@classmethod
44+
def validate_risk_details(cls, v):
45+
return str(v)[:500]

cache/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Empty init file for cache package

0 commit comments

Comments
 (0)