ardvane 0.2.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.
- ardvane/CHANGELOG.md +50 -0
- ardvane/LICENSE +29 -0
- ardvane/README.md +202 -0
- ardvane/__init__.py +1 -0
- ardvane/__main__.py +21 -0
- ardvane/app/backend/__init__.py +1 -0
- ardvane/app/backend/actions.py +90 -0
- ardvane/app/backend/cli.py +146 -0
- ardvane/app/backend/config.py +33 -0
- ardvane/app/backend/docs.py +41 -0
- ardvane/app/backend/fixes.py +53 -0
- ardvane/app/backend/launcher.py +98 -0
- ardvane/app/backend/reports.py +69 -0
- ardvane/app/backend/scanjob.py +169 -0
- ardvane/app/backend/server.py +394 -0
- ardvane/app/backend/settings.py +66 -0
- ardvane/app/backend/status.py +128 -0
- ardvane/app/backend/system.py +82 -0
- ardvane/app/backend/tasks.py +220 -0
- ardvane/app/backend/tasks_catalog.py +171 -0
- ardvane/app/dashboard.py +11 -0
- ardvane/app/tests/test_backend.py +322 -0
- ardvane/app/ui/app.css +455 -0
- ardvane/app/ui/icon.ico +0 -0
- ardvane/app/ui/icon.png +0 -0
- ardvane/app/ui/index.html +64 -0
- ardvane/app/ui/js/actions.js +149 -0
- ardvane/app/ui/js/api.js +40 -0
- ardvane/app/ui/js/charts.js +128 -0
- ardvane/app/ui/js/components.js +78 -0
- ardvane/app/ui/js/dom.js +83 -0
- ardvane/app/ui/js/feedback.js +52 -0
- ardvane/app/ui/js/fixes.js +117 -0
- ardvane/app/ui/js/format.js +36 -0
- ardvane/app/ui/js/icons.js +58 -0
- ardvane/app/ui/js/main.js +68 -0
- ardvane/app/ui/js/markdown.js +87 -0
- ardvane/app/ui/js/pages/learn.js +68 -0
- ardvane/app/ui/js/pages/overview.js +120 -0
- ardvane/app/ui/js/pages/protection.js +48 -0
- ardvane/app/ui/js/pages/reports.js +39 -0
- ardvane/app/ui/js/pages/scan.js +128 -0
- ardvane/app/ui/js/pages/settings.js +120 -0
- ardvane/app/ui/js/pages/todo.js +131 -0
- ardvane/app/ui/js/store.js +139 -0
- ardvane/app/ui/js/theme.js +29 -0
- ardvane/app/ui/jsconfig.json +16 -0
- ardvane/docs/after-a-scan/README.md +19 -0
- ardvane/docs/after-a-scan/clean-github-repos.md +78 -0
- ardvane/docs/after-a-scan/hardening-checklist.md +46 -0
- ardvane/docs/after-a-scan/if-danger-found.md +79 -0
- ardvane/docs/after-a-scan/secrets-rotation-checklist.md +53 -0
- ardvane/docs/ai-assistant-routine.md +43 -0
- ardvane/docs/attacks/README.md +30 -0
- ardvane/docs/attacks/ai-agent-config-attacks.md +40 -0
- ardvane/docs/attacks/beavertail-invisibleferret-stealers.md +41 -0
- ardvane/docs/attacks/clickfix-fake-meeting-fix.md +33 -0
- ardvane/docs/attacks/contagious-interview-fake-jobs.md +44 -0
- ardvane/docs/attacks/etherhiding-blockchain-c2.md +33 -0
- ardvane/docs/attacks/glassworm-malicious-extensions.md +34 -0
- ardvane/docs/attacks/malicious-npm-packages.md +39 -0
- ardvane/docs/attacks/microsoftsystem64-implant.md +42 -0
- ardvane/docs/attacks/polinrider-worm.md +65 -0
- ardvane/docs/attacks/vscode-tasks-autorun.md +37 -0
- ardvane/docs/checkrepo.md +120 -0
- ardvane/docs/daily-routine.md +55 -0
- ardvane/docs/dashboard.md +143 -0
- ardvane/docs/infected-packages.md +47 -0
- ardvane/docs/story/README.md +22 -0
- ardvane/docs/story/lessons.md +28 -0
- ardvane/docs/story/timeline.md +72 -0
- ardvane/docs/story/what-i-tried.md +31 -0
- ardvane/mac/daily-check.sh +169 -0
- ardvane/mac/install-routine.sh +47 -0
- ardvane/mac/install.sh +163 -0
- ardvane/mac/uninstall.sh +58 -0
- ardvane/scan-my-machine.ps1 +183 -0
- ardvane/scan-my-machine.sh +234 -0
- ardvane/scanner/autofix.py +559 -0
- ardvane/scanner/check-my-projects.py +113 -0
- ardvane/scanner/checkrepo.py +646 -0
- ardvane/scanner/github_check.py +232 -0
- ardvane/scanner/hooks/dispatch +48 -0
- ardvane/scanner/kitlib.py +66 -0
- ardvane/scanner/lists/malware-paths.txt +33 -0
- ardvane/scanner/lists/npm.txt +93 -0
- ardvane/scanner/lists/packagist.txt +19 -0
- ardvane/scanner/lists/pypi.txt +11 -0
- ardvane/scanner/safeclone/git-safeclone +71 -0
- ardvane/scanner/tests/run_tests.py +201 -0
- ardvane/scanner/tests/test_autofix.py +203 -0
- ardvane/scanner/tests/test_github_check.py +70 -0
- ardvane/windows/daily-check.ps1 +161 -0
- ardvane/windows/install-routine.ps1 +29 -0
- ardvane/windows/install.ps1 +169 -0
- ardvane/windows/safeclone.ps1 +99 -0
- ardvane/windows/uninstall.ps1 +50 -0
- ardvane-0.2.0.dist-info/METADATA +223 -0
- ardvane-0.2.0.dist-info/RECORD +102 -0
- ardvane-0.2.0.dist-info/WHEEL +4 -0
- ardvane-0.2.0.dist-info/entry_points.txt +2 -0
- ardvane-0.2.0.dist-info/licenses/LICENSE +29 -0
ardvane/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Each version's section becomes its GitHub release notes automatically. To release, raise `VERSION` in
|
|
4
|
+
`app/backend/config.py`, add a section here, and merge the pull request.
|
|
5
|
+
|
|
6
|
+
## 0.2.0
|
|
7
|
+
|
|
8
|
+
**Install Ardvane with one command.** No cloning needed.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
brew install abdul-moiz31/ardvane/ardvane # macOS (Homebrew)
|
|
12
|
+
pipx install ardvane # macOS, Linux and Windows
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then run `ardvane` to open the dashboard.
|
|
16
|
+
|
|
17
|
+
- New terminal commands: `ardvane scan`, `ardvane check <folder>`, `ardvane protect`, `ardvane unprotect`,
|
|
18
|
+
`ardvane fixes`, `ardvane launcher`. Run `ardvane --help` for all of them.
|
|
19
|
+
- Published to PyPI automatically with every release (trusted publishing, no stored passwords or tokens), and to
|
|
20
|
+
Homebrew through the `abdul-moiz31/ardvane` tap, which follows releases by itself.
|
|
21
|
+
- The app icon now opens the installed `ardvane` command, so it keeps working after upgrades.
|
|
22
|
+
|
|
23
|
+
## 0.1.0
|
|
24
|
+
|
|
25
|
+
The first release of **Ardvane**: free, open-source protection for developers against malware that spreads through code
|
|
26
|
+
(fake job interviews, malicious packages and the PolinRider worm). Everything runs on your own computer.
|
|
27
|
+
|
|
28
|
+
**Get started**
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/Abdul-Moiz31/ardvane.git
|
|
32
|
+
cd ardvane
|
|
33
|
+
python3 app/dashboard.py
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**What's in 0.1.0**
|
|
37
|
+
|
|
38
|
+
- **Ardvane dashboard** — status at a glance, scans with live progress, 30-day history, protection switches, reports
|
|
39
|
+
and offline guides. Runs on `127.0.0.1` only.
|
|
40
|
+
- **Scans your computer** — malware locations, running malware, startup items, attacker connections, editor settings,
|
|
41
|
+
every git project and installed package.
|
|
42
|
+
- **Blocks infected code** — safe `git clone`, scans on pull / checkout, blocks infected pushes.
|
|
43
|
+
- **Hardening** — editors can't auto-run code from a folder; npm packages can't run install scripts.
|
|
44
|
+
- **Fixes with undo** — ask first (default) or automatic. Malware is quarantined, never deleted.
|
|
45
|
+
- **To-do list** — every finding becomes practical steps; a later scan checks your fix worked; weekly GitHub-account checks.
|
|
46
|
+
- **Daily check** at the time you choose, with a notification.
|
|
47
|
+
- **GitHub account check** (optional) — uses the GitHub CLI when you're logged in; your token stays there.
|
|
48
|
+
|
|
49
|
+
Platforms: macOS and Linux; Windows is beta. Requires Python 3.8+ and git, nothing else.
|
|
50
|
+
Found a false alarm or tested on Windows? Please open an issue.
|
ardvane/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Abdul Moiz
|
|
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
|
+
---
|
|
24
|
+
|
|
25
|
+
Indicator lists in scanner/lists/ and parts of the detection indicators in
|
|
26
|
+
scanner/checkrepo.py are derived from:
|
|
27
|
+
- polinrider-cleaner (https://github.com/meSingh/polinrider-cleaner), MIT License
|
|
28
|
+
- Bedri-B/polinrider (https://github.com/Bedri-B/polinrider), MIT License
|
|
29
|
+
Copyright of those works remains with their respective authors.
|
ardvane/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="app/ui/icon.png" alt="" width="96" height="96">
|
|
4
|
+
|
|
5
|
+
# Ardvane
|
|
6
|
+
|
|
7
|
+
**Free, open-source protection for developers against malware that spreads through code:
|
|
8
|
+
fake job interviews, malicious packages and the PolinRider worm.**
|
|
9
|
+
|
|
10
|
+
Find it · block it · fix it · and stay on top of it every day. Everything runs on your own computer.
|
|
11
|
+
|
|
12
|
+
[](https://github.com/Abdul-Moiz31/ardvane/actions/workflows/tests.yml)
|
|
13
|
+
[](https://github.com/Abdul-Moiz31/ardvane/releases)
|
|
14
|
+
[](LICENSE)
|
|
15
|
+
-blue)
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
[Get started](#get-started) ·
|
|
19
|
+
[What it does](#what-it-does) ·
|
|
20
|
+
[How it protects you](#how-it-protects-you) ·
|
|
21
|
+
[Accuracy](#accuracy) ·
|
|
22
|
+
[I think I'm infected](docs/after-a-scan/if-danger-found.md) ·
|
|
23
|
+
[Contributing](CONTRIBUTING.md)
|
|
24
|
+
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Why Ardvane exists
|
|
30
|
+
|
|
31
|
+
Developers are being targeted by state-sponsored malware (North Korea's *Contagious Interview* campaign) that spreads
|
|
32
|
+
**through code**: fake recruiters with "take-home tests", fake npm packages and malicious editor extensions. Once inside,
|
|
33
|
+
a worm uses your stolen GitHub access to **force-push itself into every repository you can reach**, including your
|
|
34
|
+
teammates' and your company's.
|
|
35
|
+
|
|
36
|
+
I was hit **three times in five weeks**. A clean reinstall lasted three days. Deleting files, rewriting history and
|
|
37
|
+
revoking tokens didn't stop it, because it kept coming back through infected repositories and collaborators. Ardvane is
|
|
38
|
+
everything that finally worked, packaged so you don't lose the weeks I did. → [Read the story](docs/story)
|
|
39
|
+
|
|
40
|
+
> 🛑 **Think you're infected right now?** Go to [`docs/after-a-scan/if-danger-found.md`](docs/after-a-scan/if-danger-found.md). The order of the steps matters.
|
|
41
|
+
|
|
42
|
+
<a id="get-started"></a>
|
|
43
|
+
|
|
44
|
+
## 🚀 Get started
|
|
45
|
+
|
|
46
|
+
**1. Install** with one command:
|
|
47
|
+
|
|
48
|
+
| | |
|
|
49
|
+
|---|---|
|
|
50
|
+
| **macOS** (Homebrew) | `brew install abdul-moiz31/ardvane/ardvane` |
|
|
51
|
+
| **macOS, Linux, Windows** (Python 3.8+) | `pipx install ardvane` |
|
|
52
|
+
|
|
53
|
+
No pipx yet? `brew install pipx` on a Mac, `py -m pip install --user pipx` on Windows, or use
|
|
54
|
+
`python3 -m pip install --user ardvane` instead.
|
|
55
|
+
|
|
56
|
+
**2. Open Ardvane:**
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ardvane
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The dashboard opens in your browser and runs **only on your computer**. Press **Scan now**, then:
|
|
63
|
+
|
|
64
|
+
- **Protection → Turn on protection**: you see every change before it's made
|
|
65
|
+
- **Overview → Daily check**: choose your time (9:00 by default)
|
|
66
|
+
- **Protection → Add app icon**: open Ardvane from Launchpad / the Start menu from now on, no terminal needed
|
|
67
|
+
|
|
68
|
+
→ [Dashboard guide](docs/dashboard.md)
|
|
69
|
+
|
|
70
|
+
**Prefer the terminal?**
|
|
71
|
+
|
|
72
|
+
| Command | What it does |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `ardvane scan` | Read-only scan of this computer and your projects (`--quick` for machine checks only) |
|
|
75
|
+
| `ardvane check <folder>` | Scan one folder before you open it: a download, a coding test, a colleague's repo |
|
|
76
|
+
| `ardvane protect` | Turn on protection (`--dry-run` shows every change first) |
|
|
77
|
+
| `ardvane unprotect` | Remove protection and restore your previous settings |
|
|
78
|
+
| `ardvane fixes` | What Ardvane can fix for you, with undo |
|
|
79
|
+
| `ardvane --help` | Everything else |
|
|
80
|
+
|
|
81
|
+
The scan ends with ✅ **CLEAN**, ⚠️ **worth a look** or 🛑 **INFECTED**, says exactly what to do next, and saves a report.
|
|
82
|
+
Update with `brew upgrade ardvane` or `pipx upgrade ardvane`.
|
|
83
|
+
|
|
84
|
+
<details>
|
|
85
|
+
<summary>Run from source instead (contributors, or no Homebrew / pipx)</summary>
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
git clone https://github.com/Abdul-Moiz31/ardvane.git
|
|
89
|
+
cd ardvane
|
|
90
|
+
python3 app/dashboard.py # = `ardvane`; add a command, e.g. python3 app/dashboard.py scan
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The scripts also work on their own: `bash scan-my-machine.sh`, `bash mac/install.sh`, `bash mac/install-routine.sh 9:00`,
|
|
94
|
+
and on Windows (beta) the matching `.ps1` files in `windows/`.
|
|
95
|
+
|
|
96
|
+
</details>
|
|
97
|
+
|
|
98
|
+
<a id="what-it-does"></a>
|
|
99
|
+
|
|
100
|
+
## ✨ What Ardvane does
|
|
101
|
+
|
|
102
|
+
| | |
|
|
103
|
+
|---|---|
|
|
104
|
+
| 🔍 **Scans your computer** | Malware locations, running malware, startup items, attacker connections, editor settings, every git project and every installed package |
|
|
105
|
+
| 🧱 **Blocks infected code** | `git clone` checks a repository *before* its files are written; `git pull` / `checkout` warn you; `git push` is blocked if a repository contains malware |
|
|
106
|
+
| ⚙️ **Hardens your tools** | VS Code / Cursor can't auto-run code when you open a folder; npm packages can't run install scripts |
|
|
107
|
+
| 🔧 **Fixes, with undo** | Fixes what's safe to fix (editor and npm settings, switched-off protection, malware in known locations), **ask first** by default or **automatic** if you choose. Malware is quarantined, never deleted |
|
|
108
|
+
| ✅ **To-do list** | Every finding becomes a short checklist; a real infection opens the recovery checklist; a later scan checks that your fix worked |
|
|
109
|
+
| ☀️ **Daily check** | Runs at the time you choose, notices if malware switched your protection off, and shows a notification |
|
|
110
|
+
| 🐙 **GitHub account check** *(optional)* | Warns about many repos pushed at once and scans recently pushed branches. Your token stays in the GitHub CLI |
|
|
111
|
+
| 📚 **Guides** | Every attack explained, and step-by-step recovery: cut off access, clean repositories, rotate secrets, harden |
|
|
112
|
+
|
|
113
|
+
<a id="how-it-protects-you"></a>
|
|
114
|
+
|
|
115
|
+
## 🛡️ How it protects you
|
|
116
|
+
|
|
117
|
+
| You do | Ardvane does |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `git clone …` in a terminal | Downloads without writing project files → scans → **blocks** the clone if infected |
|
|
120
|
+
| `git pull`, `git checkout` | Scans and warns you before you open anything |
|
|
121
|
+
| `git push` | **Blocks** the push if the repository contains malware |
|
|
122
|
+
| Open a folder in VS Code / Cursor | Nothing runs on its own (automatic tasks off, Workspace Trust on) |
|
|
123
|
+
| `npm install` | Packages can't run install scripts |
|
|
124
|
+
| Every day at your time | Checks your machine, projects, protections and (optionally) GitHub, then adds anything it finds to your to-do list |
|
|
125
|
+
| `checkrepo <folder>` | Scan anything before you open it: a download, a coding test, a colleague's repo |
|
|
126
|
+
|
|
127
|
+
A scan takes about **0.1–0.7 seconds per repository**. When everything is clean you won't notice it.
|
|
128
|
+
|
|
129
|
+
## 🔒 Safe by design
|
|
130
|
+
|
|
131
|
+
- **Local only.** No account, no telemetry, no cloud. The dashboard listens on `127.0.0.1`, needs a per-session key,
|
|
132
|
+
and refuses requests from other websites.
|
|
133
|
+
- **Reads, doesn't run.** The scanner only reads files and runs git with hooks and `core.fsmonitor` disabled, so even a
|
|
134
|
+
booby-trapped repository can't make it execute anything.
|
|
135
|
+
- **Nothing deleted, everything reversible.** You see every change before it's made, fixes can be undone, and malware is
|
|
136
|
+
quarantined as evidence.
|
|
137
|
+
- **No dependencies.** Python's standard library and plain JavaScript. No npm, no build step, no CDN: what you read
|
|
138
|
+
here is exactly what runs.
|
|
139
|
+
- **No real malware in this repository.** Tests build synthetic samples at runtime.
|
|
140
|
+
|
|
141
|
+
<a id="accuracy"></a>
|
|
142
|
+
|
|
143
|
+
## 📊 Accuracy
|
|
144
|
+
|
|
145
|
+
The scanner was built from **real malware files** taken from an actual infection, then tuned against clean code:
|
|
146
|
+
|
|
147
|
+
| Test | Result |
|
|
148
|
+
|---|---|
|
|
149
|
+
| Real malware files (both payload variants, fake fonts, auto-run tasks, droppers) | **71 / 71 detected** |
|
|
150
|
+
| Same files with every known marker switched off (behaviour rules only) | **71 / 71 detected** |
|
|
151
|
+
| 18 popular open-source projects, 106,225 files (Font-Awesome, Babel, Webpack, Prettier, javascript-obfuscator…) | **0 false alarms** |
|
|
152
|
+
| A developer's 14 real projects + all installed packages, ~240,000 files | **0 false alarms** · 1 real infection leftover found |
|
|
153
|
+
| Automated tests: scanner, fixer (incl. attempts to trick it), dashboard security, GitHub check | **97 tests**, run on macOS, Linux and Windows on every change, plus a build-and-install test of the package |
|
|
154
|
+
|
|
155
|
+
**Honest limits:** Ardvane targets this family of attacks, so a completely new technique may not be detected. That's
|
|
156
|
+
why it also hardens your editor, npm, git and GitHub and checks every day. The Windows scripts are beta. A scan can't
|
|
157
|
+
prove a machine is clean after a backdoor has run; see the [recovery guide](docs/after-a-scan/if-danger-found.md).
|
|
158
|
+
|
|
159
|
+
## 📁 What's in the repository
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
ardvane/
|
|
163
|
+
├── app/ the Ardvane dashboard and `ardvane` command (Python standard library + plain JavaScript)
|
|
164
|
+
├── packaging/ builds the PyPI package (standard library only)
|
|
165
|
+
├── scan-my-machine.sh / .ps1 one-command, read-only machine scan
|
|
166
|
+
├── scanner/ detection and fixes: checkrepo, autofix, GitHub check, package lists, git hooks, safe clone, tests
|
|
167
|
+
├── mac/ install / uninstall / daily check for macOS and Linux
|
|
168
|
+
├── windows/ the same for Windows (beta)
|
|
169
|
+
└── docs/ guides: the attacks, recovery, hardening, the story
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## 📚 Learn more
|
|
173
|
+
|
|
174
|
+
- [The attacks](docs/attacks): PolinRider worm, fake job interviews, BeaverTail / InvisibleFerret stealers, the
|
|
175
|
+
MicrosoftSystem64 backdoor, VS Code auto-run tasks, GlassWorm extensions, malicious npm packages, AI coding-agent
|
|
176
|
+
attacks, ClickFix, EtherHiding
|
|
177
|
+
- [If you're infected](docs/after-a-scan/if-danger-found.md) · [Cleaning GitHub repositories](docs/after-a-scan/clean-github-repos.md) ·
|
|
178
|
+
[Secrets rotation](docs/after-a-scan/secrets-rotation-checklist.md) · [Hardening checklist](docs/after-a-scan/hardening-checklist.md)
|
|
179
|
+
- Reference: [Dashboard](docs/dashboard.md) · [checkrepo](docs/checkrepo.md) · [Daily check](docs/daily-routine.md) · [Infected packages](docs/infected-packages.md)
|
|
180
|
+
|
|
181
|
+
## 🤝 Contributing
|
|
182
|
+
|
|
183
|
+
Contributions are very welcome, especially **Windows test reports**, **new malicious packages or indicators** (with a
|
|
184
|
+
link to the public report) and **false-alarm reports**. Read [CONTRIBUTING.md](CONTRIBUTING.md), fork, branch, and open a
|
|
185
|
+
pull request; every change needs a test.
|
|
186
|
+
|
|
187
|
+
Found a security problem in Ardvane itself? Please report it privately; see [SECURITY.md](SECURITY.md).
|
|
188
|
+
|
|
189
|
+
## 🙏 Credits
|
|
190
|
+
|
|
191
|
+
Ardvane stands on the work of researchers and other victims who published their findings:
|
|
192
|
+
[OpenSourceMalware](https://github.com/OpenSourceMalware/PolinRider) ·
|
|
193
|
+
[polinrider-cleaner](https://github.com/meSingh/polinrider-cleaner) (MIT, indicator lists) ·
|
|
194
|
+
[Bedri-B/polinrider](https://github.com/Bedri-B/polinrider) (MIT) ·
|
|
195
|
+
[pushwarden](https://github.com/FaheemRafiq/pushwarden) · [gitshield](https://github.com/mirzasaikatahmmed/gitshield) ·
|
|
196
|
+
Palo Alto Networks Unit 42 · Google Threat Intelligence Group · JFrog Security Research · Socket · Elastic Security Labs ·
|
|
197
|
+
Checkmarx · Cloud Security Alliance. Full source lists are at the end of every page in [`docs/attacks`](docs/attacks).
|
|
198
|
+
|
|
199
|
+
## ⚖️ License and disclaimer
|
|
200
|
+
|
|
201
|
+
[MIT](LICENSE): use it, share it, adapt it.
|
|
202
|
+
Ardvane reduces risk; it cannot guarantee that a machine is clean or will stay clean. It is provided as-is, without warranty.
|
ardvane/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Ardvane: protection against malware that spreads through code. Run `ardvane --help`."""
|
ardvane/__main__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""The `ardvane` command (and `python -m ardvane`) for the installed package (pipx / pip).
|
|
2
|
+
|
|
3
|
+
The package carries the whole kit (app, scanner, scripts, guides) next to this file, so every feature works exactly
|
|
4
|
+
as it does from a clone of the repository.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
os.environ["ARDVANE_KIT"] = HERE # where the kit's files are
|
|
14
|
+
os.environ["ARDVANE_INSTALLED"] = "1" # tells the app it runs as the installed `ardvane` command
|
|
15
|
+
sys.path.insert(0, os.path.join(HERE, "app"))
|
|
16
|
+
from backend.cli import main as cli_main # noqa: E402 (needs the path above)
|
|
17
|
+
return cli_main(sys.argv[1:])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Backend of the Ardvane dashboard (Python 3.8+, standard library only)."""
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Changes the user asked for: turn protection on/off, schedule the daily check, show a file, pick a folder.
|
|
2
|
+
|
|
3
|
+
Each one runs the kit's own, reviewed scripts — the dashboard never edits system settings itself.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import threading
|
|
8
|
+
|
|
9
|
+
from .config import HOME, IS_MAC, IS_WIN, KIT
|
|
10
|
+
from .system import powershell, powershell_command, run
|
|
11
|
+
|
|
12
|
+
_picker_lock = threading.Lock()
|
|
13
|
+
PICKER_PROMPT = "Choose a folder with your projects"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def scan_command(quick=False, folders=()):
|
|
17
|
+
"""The one-command machine scan for this system (used by the dashboard and by `ardvane scan`)."""
|
|
18
|
+
if IS_WIN:
|
|
19
|
+
return powershell_command(os.path.join(KIT, "scan-my-machine.ps1"), *(["-Quick"] if quick else []), *folders)
|
|
20
|
+
return ["bash", os.path.join(KIT, "scan-my-machine.sh"), *(["--quick"] if quick else []), *folders]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def installer_command(action, dry_run=False):
|
|
24
|
+
"""action: 'install' or 'uninstall'; dry_run previews an install without changing anything."""
|
|
25
|
+
if action not in ("install", "uninstall"):
|
|
26
|
+
raise ValueError("bad action")
|
|
27
|
+
preview = dry_run and action == "install"
|
|
28
|
+
if IS_WIN:
|
|
29
|
+
return powershell_command(os.path.join(KIT, "windows", action + ".ps1"), *(["-DryRun"] if preview else []))
|
|
30
|
+
return ["bash", os.path.join(KIT, "mac", action + ".sh"), *(["--dry-run"] if preview else [])]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def installer(action, dry_run=False):
|
|
34
|
+
"""Run the installer and capture its output (for the dashboard). Returns (exit code, output)."""
|
|
35
|
+
return run(installer_command(action, dry_run), timeout=600)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def set_routine(on, at="09:00"):
|
|
39
|
+
if IS_WIN:
|
|
40
|
+
return powershell(os.path.join(KIT, "windows", "install-routine.ps1"), *(["-At", at] if on else ["-Remove"]), timeout=120)
|
|
41
|
+
if IS_MAC:
|
|
42
|
+
return run(["bash", os.path.join(KIT, "mac", "install-routine.sh")] + ([at] if on else ["--remove"]), timeout=120)
|
|
43
|
+
return 1, ("The scheduled daily check is available on macOS and Windows. On Linux, add this line with `crontab -e`:\n"
|
|
44
|
+
f"0 9 * * * bash {os.path.join(KIT, 'scan-my-machine.sh')} --quick")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _picker_command():
|
|
48
|
+
"""The system's own folder picker, or None when there isn't one (some Linux setups)."""
|
|
49
|
+
if IS_MAC:
|
|
50
|
+
return ["osascript", "-e", "activate", "-e", f'POSIX path of (choose folder with prompt "{PICKER_PROMPT}")']
|
|
51
|
+
if IS_WIN:
|
|
52
|
+
script = ("Add-Type -AssemblyName System.Windows.Forms; $d = New-Object System.Windows.Forms.FolderBrowserDialog; "
|
|
53
|
+
f"$d.Description = '{PICKER_PROMPT}'; $d.ShowNewFolderButton = $false; "
|
|
54
|
+
"$top = New-Object System.Windows.Forms.Form -Property @{TopMost = $true}; "
|
|
55
|
+
"if ($d.ShowDialog($top) -eq 'OK') { $d.SelectedPath }")
|
|
56
|
+
return ["powershell", "-NoProfile", "-STA", "-Command", script]
|
|
57
|
+
if shutil.which("zenity"):
|
|
58
|
+
return ["zenity", "--file-selection", "--directory", f"--title={PICKER_PROMPT}"]
|
|
59
|
+
if shutil.which("kdialog"):
|
|
60
|
+
return ["kdialog", "--getexistingdirectory", HOME, "--title", PICKER_PROMPT]
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def pick_folder(runner=run):
|
|
65
|
+
"""Open the native folder picker. Returns {"path": "~/..."} | {"cancelled": True} | {"unavailable": True} | {"busy": True}."""
|
|
66
|
+
cmd = _picker_command()
|
|
67
|
+
if cmd is None:
|
|
68
|
+
return {"unavailable": True}
|
|
69
|
+
if not _picker_lock.acquire(blocking=False): # one picker at a time
|
|
70
|
+
return {"busy": True}
|
|
71
|
+
try:
|
|
72
|
+
code, out = runner(cmd, timeout=600)
|
|
73
|
+
finally:
|
|
74
|
+
_picker_lock.release()
|
|
75
|
+
path = (out.strip().splitlines() or [""])[-1].strip()
|
|
76
|
+
if code != 0 or not path or not os.path.isabs(path) or not os.path.isdir(path):
|
|
77
|
+
return {"cancelled": True}
|
|
78
|
+
path = os.path.normpath(path)
|
|
79
|
+
if path == HOME or path.startswith(HOME + os.sep):
|
|
80
|
+
path = "~" + path[len(HOME):] # shorter to read, and the scripts understand ~
|
|
81
|
+
return {"path": path}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def reveal(path):
|
|
85
|
+
"""Show a file or folder in Finder / Explorer / the file manager."""
|
|
86
|
+
if IS_MAC:
|
|
87
|
+
return run(["open", "-R", path], timeout=15)
|
|
88
|
+
if IS_WIN:
|
|
89
|
+
return run(["explorer", "/select," + os.path.normpath(path)], timeout=15)
|
|
90
|
+
return run(["xdg-open", path if os.path.isdir(path) else os.path.dirname(path)], timeout=15)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Start / reuse the dashboard, and the `launcher` sub-command."""
|
|
2
|
+
import http.server
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import secrets
|
|
6
|
+
import signal
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
import urllib.request
|
|
11
|
+
import webbrowser
|
|
12
|
+
|
|
13
|
+
import subprocess
|
|
14
|
+
|
|
15
|
+
from . import actions, launcher
|
|
16
|
+
from .config import APP_NAME, IDLE_LIMIT, INSTALLED, INSTANCE, KIT, VERSION
|
|
17
|
+
from .server import App, Handler
|
|
18
|
+
from .system import python_exe, read_json, write_private
|
|
19
|
+
|
|
20
|
+
PROG = "ardvane" if INSTALLED else "python3 app/dashboard.py"
|
|
21
|
+
USAGE = f"""{APP_NAME} {VERSION}: protection against malware that spreads through code. Runs only on this computer.
|
|
22
|
+
|
|
23
|
+
{PROG} open the dashboard (add --no-browser to print its address instead)
|
|
24
|
+
{PROG} scan [--quick] [FOLDER ...] read-only scan of this computer and your projects
|
|
25
|
+
{PROG} check [FOLDER] scan one folder before you open it: a download, a coding test, a colleague's repo
|
|
26
|
+
{PROG} protect [--dry-run] turn on protection: git hooks, safe clone, npm and editor hardening
|
|
27
|
+
{PROG} unprotect remove protection and restore your previous settings
|
|
28
|
+
{PROG} fixes [list | history | fix ID | undo ID]
|
|
29
|
+
fixes Ardvane can make for you, with undo
|
|
30
|
+
{PROG} launcher [--remove] add (or remove) the app icon
|
|
31
|
+
{PROG} --version
|
|
32
|
+
|
|
33
|
+
The dashboard runs on 127.0.0.1 and stops by itself a few minutes after you close its tab.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def running_instance():
|
|
38
|
+
"""Address of a dashboard that is already running (so a second click reuses it), or None."""
|
|
39
|
+
info = read_json(INSTANCE, {})
|
|
40
|
+
port, token = info.get("port"), info.get("token")
|
|
41
|
+
if not isinstance(port, int) or not isinstance(token, str):
|
|
42
|
+
return None
|
|
43
|
+
try:
|
|
44
|
+
req = urllib.request.Request(f"http://127.0.0.1:{port}/api/ping", headers={"X-Guard-Key": token})
|
|
45
|
+
with urllib.request.urlopen(req, timeout=2) as r:
|
|
46
|
+
return f"http://127.0.0.1:{port}/#k={token}" if r.status == 200 else None
|
|
47
|
+
except Exception:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _watchdog():
|
|
52
|
+
while True:
|
|
53
|
+
time.sleep(15)
|
|
54
|
+
scanning = App.job is not None and App.job.finished is None
|
|
55
|
+
if not scanning and not App.busy and time.time() - App.last_seen > IDLE_LIMIT:
|
|
56
|
+
App.httpd.shutdown()
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _stop_on_sigterm(signum, frame):
|
|
61
|
+
"""Stopped by the system or `kill`: shut down cleanly, exactly like Ctrl+C."""
|
|
62
|
+
raise KeyboardInterrupt
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def serve(open_browser=True, port=0):
|
|
66
|
+
url = running_instance()
|
|
67
|
+
if url:
|
|
68
|
+
print(f"{APP_NAME} is already open.", flush=True)
|
|
69
|
+
if open_browser:
|
|
70
|
+
webbrowser.open(url)
|
|
71
|
+
return 0
|
|
72
|
+
App.token = secrets.token_urlsafe(32)
|
|
73
|
+
App.httpd = http.server.ThreadingHTTPServer(("127.0.0.1", port), Handler)
|
|
74
|
+
App.httpd.daemon_threads = True
|
|
75
|
+
App.port = App.httpd.server_address[1]
|
|
76
|
+
App.last_seen = time.time()
|
|
77
|
+
write_private(INSTANCE, json.dumps({"port": App.port, "token": App.token, "pid": os.getpid()}))
|
|
78
|
+
url = f"http://127.0.0.1:{App.port}/#k={App.token}"
|
|
79
|
+
threading.Thread(target=_watchdog, daemon=True).start()
|
|
80
|
+
signal.signal(signal.SIGTERM, _stop_on_sigterm)
|
|
81
|
+
print(f"{APP_NAME} is running on this computer only (port {App.port}).", flush=True)
|
|
82
|
+
print("It stops by itself a few minutes after you close the browser tab, or press Ctrl+C.", flush=True)
|
|
83
|
+
if open_browser:
|
|
84
|
+
webbrowser.open(url)
|
|
85
|
+
else:
|
|
86
|
+
print("Open this address — it contains your private key, don't share it:", url, flush=True)
|
|
87
|
+
try:
|
|
88
|
+
App.httpd.serve_forever(poll_interval=0.5)
|
|
89
|
+
except KeyboardInterrupt:
|
|
90
|
+
pass
|
|
91
|
+
finally:
|
|
92
|
+
if App.job:
|
|
93
|
+
App.job.cancel()
|
|
94
|
+
if read_json(INSTANCE, {}).get("pid") == os.getpid():
|
|
95
|
+
try:
|
|
96
|
+
os.remove(INSTANCE)
|
|
97
|
+
except OSError:
|
|
98
|
+
pass
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _call(cmd):
|
|
103
|
+
"""Run one of the kit's own scripts in this terminal and return its exit code."""
|
|
104
|
+
try:
|
|
105
|
+
return subprocess.call(cmd)
|
|
106
|
+
except KeyboardInterrupt:
|
|
107
|
+
return 130
|
|
108
|
+
except OSError as e:
|
|
109
|
+
print(f"{APP_NAME}: could not start {cmd[0]}: {e}", file=sys.stderr)
|
|
110
|
+
return 3
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _scanner(name, args):
|
|
114
|
+
return _call([python_exe(), os.path.join(KIT, "scanner", name), *args])
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# `ardvane <command> ...`: each command runs the same reviewed script the dashboard uses
|
|
118
|
+
COMMANDS = {
|
|
119
|
+
"scan": lambda a: _call(actions.scan_command("--quick" in a, [x for x in a if x != "--quick"])),
|
|
120
|
+
"check": lambda a: _scanner("checkrepo.py", a),
|
|
121
|
+
"protect": lambda a: _call(actions.installer_command("install", "--dry-run" in a)),
|
|
122
|
+
"unprotect": lambda a: _call(actions.installer_command("uninstall")),
|
|
123
|
+
"fixes": lambda a: _scanner("autofix.py", a or ["list"]),
|
|
124
|
+
"launcher": lambda a: print((launcher.remove() if "--remove" in a else launcher.install())[1]) or 0,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def main(argv):
|
|
129
|
+
for stream in (sys.stdout, sys.stderr):
|
|
130
|
+
try:
|
|
131
|
+
stream.reconfigure(errors="replace")
|
|
132
|
+
except (AttributeError, ValueError):
|
|
133
|
+
pass
|
|
134
|
+
if argv[:1] in (["-h"], ["--help"], ["help"]):
|
|
135
|
+
print(USAGE)
|
|
136
|
+
return 0
|
|
137
|
+
if argv[:1] in (["--version"], ["version"]):
|
|
138
|
+
print(APP_NAME, VERSION)
|
|
139
|
+
return 0
|
|
140
|
+
if argv and argv[0] in COMMANDS:
|
|
141
|
+
return COMMANDS[argv[0]](argv[1:])
|
|
142
|
+
unknown = [a for a in argv if a != "--no-browser"]
|
|
143
|
+
if unknown:
|
|
144
|
+
print(f"unknown command: {unknown[0]}\n\n{USAGE}", file=sys.stderr)
|
|
145
|
+
return 3
|
|
146
|
+
return serve(open_browser="--no-browser" not in argv)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Names, paths and constants shared by the whole backend."""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
APP_NAME = "Ardvane" # the product name lives here and in ui/index.html
|
|
6
|
+
APP_ID = "ardvane"
|
|
7
|
+
VERSION = "0.2.0"
|
|
8
|
+
|
|
9
|
+
APP_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
10
|
+
KIT = os.environ.get("ARDVANE_KIT") or os.environ.get("POLINRIDER_KIT") or os.path.dirname(APP_DIR)
|
|
11
|
+
INSTALLED = bool(os.environ.get("ARDVANE_INSTALLED")) # started by the installed `ardvane` command (pipx / Homebrew)
|
|
12
|
+
UI = os.path.join(APP_DIR, "ui")
|
|
13
|
+
|
|
14
|
+
HOME = os.path.expanduser("~")
|
|
15
|
+
DEST = os.path.join(HOME, ".checkrepo") # where the installer puts the scanner, hooks and reports
|
|
16
|
+
REPORTS = os.path.join(DEST, "reports")
|
|
17
|
+
SCANS = os.path.join(REPORTS, "scans")
|
|
18
|
+
SETTINGS = os.path.join(DEST, "settings.json")
|
|
19
|
+
PROJECTS = os.path.join(DEST, "projects.txt")
|
|
20
|
+
INSTANCE = os.path.join(DEST, "dashboard.json")
|
|
21
|
+
TASKS = os.path.join(DEST, "tasks.json")
|
|
22
|
+
|
|
23
|
+
IS_WIN = os.name == "nt"
|
|
24
|
+
IS_MAC = sys.platform == "darwin"
|
|
25
|
+
OS_NAME = "windows" if IS_WIN else "mac" if IS_MAC else "linux"
|
|
26
|
+
|
|
27
|
+
# These three keep the project's original name on purpose: installs already on people's computers use them.
|
|
28
|
+
ROUTINE_LABEL = "com.polinrider-survival-kit.daily-check" # macOS launchd job (mac/install-routine.sh)
|
|
29
|
+
ROUTINE_TASK = "polinrider-survival-kit daily check" # Windows scheduled task (windows/install-routine.ps1)
|
|
30
|
+
RC_MARKER = "# >>> polinrider-survival-kit >>>" # block the installer adds to shell profiles
|
|
31
|
+
|
|
32
|
+
IDLE_LIMIT = 300 # seconds without a request from the tab before the dashboard stops itself
|
|
33
|
+
DEFAULT_FOLDERS = ["~/Documents", "~/Desktop", "~/Downloads", "~/code", "~/projects"] + (["~/source/repos"] if IS_WIN else [])
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""The kit's own guides (docs/*.md), served read-only to the Learn page."""
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from .config import KIT
|
|
5
|
+
|
|
6
|
+
GROUPS = [("If you're infected", "after-a-scan"), ("The attacks", "attacks"), ("The story", "story"), ("Reference", "")]
|
|
7
|
+
ROOT = os.path.join(KIT, "docs")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _title(full, fallback):
|
|
11
|
+
try:
|
|
12
|
+
with open(full, encoding="utf-8") as f:
|
|
13
|
+
for line in f:
|
|
14
|
+
if line.startswith("# "):
|
|
15
|
+
return line[2:].strip()
|
|
16
|
+
except OSError:
|
|
17
|
+
pass
|
|
18
|
+
return fallback
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def list_all():
|
|
22
|
+
out = []
|
|
23
|
+
for group, sub in GROUPS:
|
|
24
|
+
folder = os.path.join(ROOT, sub)
|
|
25
|
+
try:
|
|
26
|
+
names = sorted(n for n in os.listdir(folder) if n.endswith(".md"))
|
|
27
|
+
except OSError:
|
|
28
|
+
continue
|
|
29
|
+
names = (["README.md"] if "README.md" in names else []) + [n for n in names if n != "README.md"]
|
|
30
|
+
for name in names:
|
|
31
|
+
rel = f"{sub}/{name}" if sub else name
|
|
32
|
+
out.append({"group": group, "path": rel, "title": _title(os.path.join(folder, name), name[:-3].replace("-", " ").capitalize())})
|
|
33
|
+
return out
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read(rel):
|
|
37
|
+
"""Text of a guide, or None. Only paths from list_all() are accepted, so nothing outside docs/ can be read."""
|
|
38
|
+
if rel not in {d["path"] for d in list_all()}:
|
|
39
|
+
return None
|
|
40
|
+
with open(os.path.join(ROOT, *rel.split("/")), encoding="utf-8") as f:
|
|
41
|
+
return f.read()
|