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.
Files changed (102) hide show
  1. ardvane/CHANGELOG.md +50 -0
  2. ardvane/LICENSE +29 -0
  3. ardvane/README.md +202 -0
  4. ardvane/__init__.py +1 -0
  5. ardvane/__main__.py +21 -0
  6. ardvane/app/backend/__init__.py +1 -0
  7. ardvane/app/backend/actions.py +90 -0
  8. ardvane/app/backend/cli.py +146 -0
  9. ardvane/app/backend/config.py +33 -0
  10. ardvane/app/backend/docs.py +41 -0
  11. ardvane/app/backend/fixes.py +53 -0
  12. ardvane/app/backend/launcher.py +98 -0
  13. ardvane/app/backend/reports.py +69 -0
  14. ardvane/app/backend/scanjob.py +169 -0
  15. ardvane/app/backend/server.py +394 -0
  16. ardvane/app/backend/settings.py +66 -0
  17. ardvane/app/backend/status.py +128 -0
  18. ardvane/app/backend/system.py +82 -0
  19. ardvane/app/backend/tasks.py +220 -0
  20. ardvane/app/backend/tasks_catalog.py +171 -0
  21. ardvane/app/dashboard.py +11 -0
  22. ardvane/app/tests/test_backend.py +322 -0
  23. ardvane/app/ui/app.css +455 -0
  24. ardvane/app/ui/icon.ico +0 -0
  25. ardvane/app/ui/icon.png +0 -0
  26. ardvane/app/ui/index.html +64 -0
  27. ardvane/app/ui/js/actions.js +149 -0
  28. ardvane/app/ui/js/api.js +40 -0
  29. ardvane/app/ui/js/charts.js +128 -0
  30. ardvane/app/ui/js/components.js +78 -0
  31. ardvane/app/ui/js/dom.js +83 -0
  32. ardvane/app/ui/js/feedback.js +52 -0
  33. ardvane/app/ui/js/fixes.js +117 -0
  34. ardvane/app/ui/js/format.js +36 -0
  35. ardvane/app/ui/js/icons.js +58 -0
  36. ardvane/app/ui/js/main.js +68 -0
  37. ardvane/app/ui/js/markdown.js +87 -0
  38. ardvane/app/ui/js/pages/learn.js +68 -0
  39. ardvane/app/ui/js/pages/overview.js +120 -0
  40. ardvane/app/ui/js/pages/protection.js +48 -0
  41. ardvane/app/ui/js/pages/reports.js +39 -0
  42. ardvane/app/ui/js/pages/scan.js +128 -0
  43. ardvane/app/ui/js/pages/settings.js +120 -0
  44. ardvane/app/ui/js/pages/todo.js +131 -0
  45. ardvane/app/ui/js/store.js +139 -0
  46. ardvane/app/ui/js/theme.js +29 -0
  47. ardvane/app/ui/jsconfig.json +16 -0
  48. ardvane/docs/after-a-scan/README.md +19 -0
  49. ardvane/docs/after-a-scan/clean-github-repos.md +78 -0
  50. ardvane/docs/after-a-scan/hardening-checklist.md +46 -0
  51. ardvane/docs/after-a-scan/if-danger-found.md +79 -0
  52. ardvane/docs/after-a-scan/secrets-rotation-checklist.md +53 -0
  53. ardvane/docs/ai-assistant-routine.md +43 -0
  54. ardvane/docs/attacks/README.md +30 -0
  55. ardvane/docs/attacks/ai-agent-config-attacks.md +40 -0
  56. ardvane/docs/attacks/beavertail-invisibleferret-stealers.md +41 -0
  57. ardvane/docs/attacks/clickfix-fake-meeting-fix.md +33 -0
  58. ardvane/docs/attacks/contagious-interview-fake-jobs.md +44 -0
  59. ardvane/docs/attacks/etherhiding-blockchain-c2.md +33 -0
  60. ardvane/docs/attacks/glassworm-malicious-extensions.md +34 -0
  61. ardvane/docs/attacks/malicious-npm-packages.md +39 -0
  62. ardvane/docs/attacks/microsoftsystem64-implant.md +42 -0
  63. ardvane/docs/attacks/polinrider-worm.md +65 -0
  64. ardvane/docs/attacks/vscode-tasks-autorun.md +37 -0
  65. ardvane/docs/checkrepo.md +120 -0
  66. ardvane/docs/daily-routine.md +55 -0
  67. ardvane/docs/dashboard.md +143 -0
  68. ardvane/docs/infected-packages.md +47 -0
  69. ardvane/docs/story/README.md +22 -0
  70. ardvane/docs/story/lessons.md +28 -0
  71. ardvane/docs/story/timeline.md +72 -0
  72. ardvane/docs/story/what-i-tried.md +31 -0
  73. ardvane/mac/daily-check.sh +169 -0
  74. ardvane/mac/install-routine.sh +47 -0
  75. ardvane/mac/install.sh +163 -0
  76. ardvane/mac/uninstall.sh +58 -0
  77. ardvane/scan-my-machine.ps1 +183 -0
  78. ardvane/scan-my-machine.sh +234 -0
  79. ardvane/scanner/autofix.py +559 -0
  80. ardvane/scanner/check-my-projects.py +113 -0
  81. ardvane/scanner/checkrepo.py +646 -0
  82. ardvane/scanner/github_check.py +232 -0
  83. ardvane/scanner/hooks/dispatch +48 -0
  84. ardvane/scanner/kitlib.py +66 -0
  85. ardvane/scanner/lists/malware-paths.txt +33 -0
  86. ardvane/scanner/lists/npm.txt +93 -0
  87. ardvane/scanner/lists/packagist.txt +19 -0
  88. ardvane/scanner/lists/pypi.txt +11 -0
  89. ardvane/scanner/safeclone/git-safeclone +71 -0
  90. ardvane/scanner/tests/run_tests.py +201 -0
  91. ardvane/scanner/tests/test_autofix.py +203 -0
  92. ardvane/scanner/tests/test_github_check.py +70 -0
  93. ardvane/windows/daily-check.ps1 +161 -0
  94. ardvane/windows/install-routine.ps1 +29 -0
  95. ardvane/windows/install.ps1 +169 -0
  96. ardvane/windows/safeclone.ps1 +99 -0
  97. ardvane/windows/uninstall.ps1 +50 -0
  98. ardvane-0.2.0.dist-info/METADATA +223 -0
  99. ardvane-0.2.0.dist-info/RECORD +102 -0
  100. ardvane-0.2.0.dist-info/WHEEL +4 -0
  101. ardvane-0.2.0.dist-info/entry_points.txt +2 -0
  102. 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
+ [![Tests](https://github.com/Abdul-Moiz31/ardvane/actions/workflows/tests.yml/badge.svg)](https://github.com/Abdul-Moiz31/ardvane/actions/workflows/tests.yml)
13
+ [![Version](https://img.shields.io/badge/version-0.1.0-blue)](https://github.com/Abdul-Moiz31/ardvane/releases)
14
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
15
+ ![Platforms](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows%20(beta)-blue)
16
+ ![Dependencies](https://img.shields.io/badge/dependencies-none-brightgreen)
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()