macverify 1.0.0__tar.gz
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.
- macverify-1.0.0/LICENSE +21 -0
- macverify-1.0.0/PKG-INFO +198 -0
- macverify-1.0.0/README.md +149 -0
- macverify-1.0.0/macverify/__init__.py +39 -0
- macverify-1.0.0/macverify/__main__.py +4 -0
- macverify-1.0.0/macverify/aicommon.py +112 -0
- macverify-1.0.0/macverify/cli.py +231 -0
- macverify-1.0.0/macverify/collectors/__init__.py +0 -0
- macverify-1.0.0/macverify/collectors/ai_assistants.py +208 -0
- macverify-1.0.0/macverify/collectors/claude_code.py +926 -0
- macverify-1.0.0/macverify/collectors/containers.py +154 -0
- macverify-1.0.0/macverify/collectors/github_copilot.py +343 -0
- macverify-1.0.0/macverify/collectors/hardware.py +309 -0
- macverify-1.0.0/macverify/collectors/identity.py +270 -0
- macverify-1.0.0/macverify/collectors/network.py +298 -0
- macverify-1.0.0/macverify/collectors/openai_codex.py +411 -0
- macverify-1.0.0/macverify/collectors/packages.py +305 -0
- macverify-1.0.0/macverify/collectors/permissions.py +163 -0
- macverify-1.0.0/macverify/collectors/secrets.py +302 -0
- macverify-1.0.0/macverify/collectors/security.py +284 -0
- macverify-1.0.0/macverify/collectors/services.py +181 -0
- macverify-1.0.0/macverify/collectors/shell_env.py +354 -0
- macverify-1.0.0/macverify/collectors/storage.py +263 -0
- macverify-1.0.0/macverify/collectors/toolchain.py +474 -0
- macverify-1.0.0/macverify/compat.py +201 -0
- macverify-1.0.0/macverify/context.py +19 -0
- macverify-1.0.0/macverify/findings.py +38 -0
- macverify-1.0.0/macverify/fsutil.py +216 -0
- macverify-1.0.0/macverify/i18n.py +244 -0
- macverify-1.0.0/macverify/quickfix.py +280 -0
- macverify-1.0.0/macverify/registry.py +40 -0
- macverify-1.0.0/macverify/report_html.py +1422 -0
- macverify-1.0.0/macverify/report_md.py +175 -0
- macverify-1.0.0/macverify/runner.py +52 -0
- macverify-1.0.0/macverify/scope.py +150 -0
- macverify-1.0.0/macverify/shell.py +136 -0
- macverify-1.0.0/macverify/sysinfo.py +128 -0
- macverify-1.0.0/macverify.egg-info/PKG-INFO +198 -0
- macverify-1.0.0/macverify.egg-info/SOURCES.txt +47 -0
- macverify-1.0.0/macverify.egg-info/dependency_links.txt +1 -0
- macverify-1.0.0/macverify.egg-info/entry_points.txt +2 -0
- macverify-1.0.0/macverify.egg-info/top_level.txt +1 -0
- macverify-1.0.0/pyproject.toml +43 -0
- macverify-1.0.0/setup.cfg +4 -0
- macverify-1.0.0/tests/test_cli.py +153 -0
- macverify-1.0.0/tests/test_degradation.py +94 -0
- macverify-1.0.0/tests/test_guarantees.py +267 -0
- macverify-1.0.0/tests/test_quickfix.py +151 -0
- macverify-1.0.0/tests/test_redaction.py +137 -0
macverify-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shane Dixon
|
|
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.
|
macverify-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: macverify
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Read-only, offline inventory of a macOS user space and its AI assistant configuration
|
|
5
|
+
Author: Shane Dixon
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Shane Dixon
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Homepage, https://github.com/ShanedixonGit/macverify
|
|
28
|
+
Project-URL: Source, https://github.com/ShanedixonGit/macverify
|
|
29
|
+
Project-URL: Issues, https://github.com/ShanedixonGit/macverify/issues
|
|
30
|
+
Keywords: macos,audit,inventory,read-only,offline,claude-code,copilot,codex
|
|
31
|
+
Classifier: Development Status :: 4 - Beta
|
|
32
|
+
Classifier: Environment :: Console
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: Intended Audience :: System Administrators
|
|
35
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
43
|
+
Classifier: Topic :: System :: Systems Administration
|
|
44
|
+
Classifier: Topic :: Utilities
|
|
45
|
+
Requires-Python: >=3.9
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
License-File: LICENSE
|
|
48
|
+
Dynamic: license-file
|
|
49
|
+
|
|
50
|
+
<picture>
|
|
51
|
+
<source media="(prefers-color-scheme: dark)" srcset=".github/brand/wordmark-dark.svg">
|
|
52
|
+
<img alt="macverify" src=".github/brand/wordmark-light.svg" width="340">
|
|
53
|
+
</picture>
|
|
54
|
+
|
|
55
|
+
[](https://github.com/ShanedixonGit/macverify/actions/workflows/ci.yml) [](LICENSE)
|
|
56
|
+
|
|
57
|
+
> **macverify 1.0.0** — macOS 11+, Python 3.9+, zero dependencies, fully offline.
|
|
58
|
+
|
|
59
|
+
A read-only, offline inventory of a Mac: what is installed, what runs at login,
|
|
60
|
+
what listens on the network, which protections are on, where credential-shaped
|
|
61
|
+
values sit in plain files, and how your AI assistants (Claude Code, GitHub
|
|
62
|
+
Copilot, OpenAI Codex) are configured and what they cost to load. It writes an
|
|
63
|
+
HTML report, a JSON dataset and a list of remediation commands, then stops.
|
|
64
|
+
|
|
65
|
+
## What this is NOT
|
|
66
|
+
|
|
67
|
+
**macverify is not antivirus. It is not malware detection. It is not real-time
|
|
68
|
+
protection. It does not scan for known threats.** It has no signature database,
|
|
69
|
+
computes no file hashes, consults no reputation service, and makes no network
|
|
70
|
+
request, so nothing it sees is ever compared against a threat feed.
|
|
71
|
+
|
|
72
|
+
It takes a point-in-time, read-only inventory of your toolchain, config hygiene
|
|
73
|
+
and AI-assistant setup, and flags things worth reviewing yourself. A launchd job
|
|
74
|
+
that exists, is signed and starts at login looks the same from here whether it
|
|
75
|
+
is a printer helper or a backdoor.
|
|
76
|
+
|
|
77
|
+
Run a real scanner alongside it: [Malwarebytes](https://www.malwarebytes.com/mac),
|
|
78
|
+
[KnockKnock](https://objective-see.org/products/knockknock.html) or
|
|
79
|
+
[LuLu](https://objective-see.org/products/lulu.html). Scan first — removing
|
|
80
|
+
adware changes what this report is built from.
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
pipx install macverify
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Or with pip, if you do not use pipx:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
pip install --user macverify
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
No build step, no dependencies. To install nothing at all, clone and run in
|
|
95
|
+
place:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
git clone https://github.com/ShanedixonGit/macverify.git
|
|
99
|
+
cd macverify
|
|
100
|
+
python3 -m macverify
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Verify it before you run it
|
|
104
|
+
|
|
105
|
+
This tool reads your machine, so do not take its word for it. From a clone,
|
|
106
|
+
these must all return nothing:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
grep -rn "shell=True\|os\.system\|os\.popen" --include='*.py' .
|
|
110
|
+
grep -rnE "\b(eval|exec)[[:space:]]*\(|pickle" --include='*.py' .
|
|
111
|
+
grep -rn "urllib\|requests\.\|http\.client\|urlopen\|httpx" --include='*.py' .
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
No shell execution, no dynamic evaluation, no network client. The same claims
|
|
115
|
+
are enforced as tests, which CI re-runs on every push:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
/usr/bin/python3 -m compileall -q macverify
|
|
119
|
+
/usr/bin/python3 -m unittest discover -s tests -t .
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
[SECURITY.md](SECURITY.md) lists each claim and how it is checked.
|
|
123
|
+
|
|
124
|
+
## Usage
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
macverify --check # what this Mac can be audited for, then exit
|
|
128
|
+
macverify # the full run
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`--check` collects nothing, and is the safe way to see what a run would cover.
|
|
132
|
+
A full run takes 30-60 seconds, writing to `~/.macverify/reports` (`0700`):
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
-h, --help show this help message and exit
|
|
136
|
+
--version print the installed version and exit
|
|
137
|
+
--only DOMAIN run only this domain (repeatable)
|
|
138
|
+
--skip DOMAIN skip this domain (repeatable)
|
|
139
|
+
--json-only write the JSON dataset only
|
|
140
|
+
--html-only write the HTML report only
|
|
141
|
+
--out DIR output directory (default: ~/.macverify/reports)
|
|
142
|
+
--timeout S per-command timeout in seconds (default: 8)
|
|
143
|
+
--lang {en,es} report label language (default: en)
|
|
144
|
+
--project PATH extra project root to inspect for AI assistant config
|
|
145
|
+
(repeatable)
|
|
146
|
+
--verbose print per-domain progress to stderr
|
|
147
|
+
--list-domains list domain names and exit
|
|
148
|
+
--quick-fixes print the quick-fix plan to stdout as well as writing the
|
|
149
|
+
reports
|
|
150
|
+
--check report what this machine can be audited for, then exit
|
|
151
|
+
without collecting
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
macverify --quick-fixes
|
|
156
|
+
macverify --only security --only network
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
A run writes `audit_<timestamp>.html` (self-contained), `audit_<timestamp>.json`
|
|
160
|
+
(full dataset), `remediation.md` and `ai_assistant_findings.json`. See
|
|
161
|
+
[examples/](examples/) for the JSON shape and how to pull findings out of it.
|
|
162
|
+
|
|
163
|
+
## What it reads, and what it never does
|
|
164
|
+
|
|
165
|
+
Sixteen domains: `toolchain`, `packages`, `shell_env`, `hardware`, `storage`,
|
|
166
|
+
`services`, `containers`, `network`, `security`, `identity`, `secrets`,
|
|
167
|
+
`permissions`, `claude_code`, `github_copilot`, `openai_codex`, `ai_assistants`.
|
|
168
|
+
Missing tools are reported as unavailable, never as an error.
|
|
169
|
+
|
|
170
|
+
It never makes a network request. It never calls `sudo` — `sudo`, `su`, `doas`,
|
|
171
|
+
`sudoedit`, `security` and `systemsetup` are refused by its command runner, so
|
|
172
|
+
no code path can elevate. It never modifies system state; fixes are printed,
|
|
173
|
+
never executed. It never captures a secret value: the secrets domain records the
|
|
174
|
+
file, line, detector and a masked prefix, never the value. It never reads AI
|
|
175
|
+
session transcripts, only their file metadata.
|
|
176
|
+
|
|
177
|
+
Reports are sensitive: an `audit_*.json` names your host, user, applications,
|
|
178
|
+
SSH key paths, privacy grants and anything credential-shaped. Read one before
|
|
179
|
+
you send it anywhere.
|
|
180
|
+
|
|
181
|
+
## Let an AI assistant triage the report
|
|
182
|
+
|
|
183
|
+
[`SKILL.md`](SKILL.md) is a self-contained skill that runs the audit, reads the
|
|
184
|
+
dataset and returns a short ranked list of fixes rather than eighty raw
|
|
185
|
+
findings. For Claude Code, copy it to `~/.claude/skills/macverify-review/`; for
|
|
186
|
+
Codex, append it to your `AGENTS.md`. It never runs a destructive command
|
|
187
|
+
without asking, and never runs `sudo` on your behalf.
|
|
188
|
+
|
|
189
|
+
## Requirements
|
|
190
|
+
|
|
191
|
+
macOS 11+ and Python 3.9+; every Mac since macOS 12.3 ships a suitable
|
|
192
|
+
`python3`. No third-party packages, at install time or runtime.
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT. See [LICENSE](LICENSE). Also [CONTRIBUTING.md](CONTRIBUTING.md),
|
|
197
|
+
[CHANGELOG.md](CHANGELOG.md), [SECURITY.md](SECURITY.md) and
|
|
198
|
+
[design_notes.md](design_notes.md).
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset=".github/brand/wordmark-dark.svg">
|
|
3
|
+
<img alt="macverify" src=".github/brand/wordmark-light.svg" width="340">
|
|
4
|
+
</picture>
|
|
5
|
+
|
|
6
|
+
[](https://github.com/ShanedixonGit/macverify/actions/workflows/ci.yml) [](LICENSE)
|
|
7
|
+
|
|
8
|
+
> **macverify 1.0.0** — macOS 11+, Python 3.9+, zero dependencies, fully offline.
|
|
9
|
+
|
|
10
|
+
A read-only, offline inventory of a Mac: what is installed, what runs at login,
|
|
11
|
+
what listens on the network, which protections are on, where credential-shaped
|
|
12
|
+
values sit in plain files, and how your AI assistants (Claude Code, GitHub
|
|
13
|
+
Copilot, OpenAI Codex) are configured and what they cost to load. It writes an
|
|
14
|
+
HTML report, a JSON dataset and a list of remediation commands, then stops.
|
|
15
|
+
|
|
16
|
+
## What this is NOT
|
|
17
|
+
|
|
18
|
+
**macverify is not antivirus. It is not malware detection. It is not real-time
|
|
19
|
+
protection. It does not scan for known threats.** It has no signature database,
|
|
20
|
+
computes no file hashes, consults no reputation service, and makes no network
|
|
21
|
+
request, so nothing it sees is ever compared against a threat feed.
|
|
22
|
+
|
|
23
|
+
It takes a point-in-time, read-only inventory of your toolchain, config hygiene
|
|
24
|
+
and AI-assistant setup, and flags things worth reviewing yourself. A launchd job
|
|
25
|
+
that exists, is signed and starts at login looks the same from here whether it
|
|
26
|
+
is a printer helper or a backdoor.
|
|
27
|
+
|
|
28
|
+
Run a real scanner alongside it: [Malwarebytes](https://www.malwarebytes.com/mac),
|
|
29
|
+
[KnockKnock](https://objective-see.org/products/knockknock.html) or
|
|
30
|
+
[LuLu](https://objective-see.org/products/lulu.html). Scan first — removing
|
|
31
|
+
adware changes what this report is built from.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pipx install macverify
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or with pip, if you do not use pipx:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
pip install --user macverify
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
No build step, no dependencies. To install nothing at all, clone and run in
|
|
46
|
+
place:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
git clone https://github.com/ShanedixonGit/macverify.git
|
|
50
|
+
cd macverify
|
|
51
|
+
python3 -m macverify
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Verify it before you run it
|
|
55
|
+
|
|
56
|
+
This tool reads your machine, so do not take its word for it. From a clone,
|
|
57
|
+
these must all return nothing:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
grep -rn "shell=True\|os\.system\|os\.popen" --include='*.py' .
|
|
61
|
+
grep -rnE "\b(eval|exec)[[:space:]]*\(|pickle" --include='*.py' .
|
|
62
|
+
grep -rn "urllib\|requests\.\|http\.client\|urlopen\|httpx" --include='*.py' .
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
No shell execution, no dynamic evaluation, no network client. The same claims
|
|
66
|
+
are enforced as tests, which CI re-runs on every push:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
/usr/bin/python3 -m compileall -q macverify
|
|
70
|
+
/usr/bin/python3 -m unittest discover -s tests -t .
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
[SECURITY.md](SECURITY.md) lists each claim and how it is checked.
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
macverify --check # what this Mac can be audited for, then exit
|
|
79
|
+
macverify # the full run
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`--check` collects nothing, and is the safe way to see what a run would cover.
|
|
83
|
+
A full run takes 30-60 seconds, writing to `~/.macverify/reports` (`0700`):
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
-h, --help show this help message and exit
|
|
87
|
+
--version print the installed version and exit
|
|
88
|
+
--only DOMAIN run only this domain (repeatable)
|
|
89
|
+
--skip DOMAIN skip this domain (repeatable)
|
|
90
|
+
--json-only write the JSON dataset only
|
|
91
|
+
--html-only write the HTML report only
|
|
92
|
+
--out DIR output directory (default: ~/.macverify/reports)
|
|
93
|
+
--timeout S per-command timeout in seconds (default: 8)
|
|
94
|
+
--lang {en,es} report label language (default: en)
|
|
95
|
+
--project PATH extra project root to inspect for AI assistant config
|
|
96
|
+
(repeatable)
|
|
97
|
+
--verbose print per-domain progress to stderr
|
|
98
|
+
--list-domains list domain names and exit
|
|
99
|
+
--quick-fixes print the quick-fix plan to stdout as well as writing the
|
|
100
|
+
reports
|
|
101
|
+
--check report what this machine can be audited for, then exit
|
|
102
|
+
without collecting
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
macverify --quick-fixes
|
|
107
|
+
macverify --only security --only network
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
A run writes `audit_<timestamp>.html` (self-contained), `audit_<timestamp>.json`
|
|
111
|
+
(full dataset), `remediation.md` and `ai_assistant_findings.json`. See
|
|
112
|
+
[examples/](examples/) for the JSON shape and how to pull findings out of it.
|
|
113
|
+
|
|
114
|
+
## What it reads, and what it never does
|
|
115
|
+
|
|
116
|
+
Sixteen domains: `toolchain`, `packages`, `shell_env`, `hardware`, `storage`,
|
|
117
|
+
`services`, `containers`, `network`, `security`, `identity`, `secrets`,
|
|
118
|
+
`permissions`, `claude_code`, `github_copilot`, `openai_codex`, `ai_assistants`.
|
|
119
|
+
Missing tools are reported as unavailable, never as an error.
|
|
120
|
+
|
|
121
|
+
It never makes a network request. It never calls `sudo` — `sudo`, `su`, `doas`,
|
|
122
|
+
`sudoedit`, `security` and `systemsetup` are refused by its command runner, so
|
|
123
|
+
no code path can elevate. It never modifies system state; fixes are printed,
|
|
124
|
+
never executed. It never captures a secret value: the secrets domain records the
|
|
125
|
+
file, line, detector and a masked prefix, never the value. It never reads AI
|
|
126
|
+
session transcripts, only their file metadata.
|
|
127
|
+
|
|
128
|
+
Reports are sensitive: an `audit_*.json` names your host, user, applications,
|
|
129
|
+
SSH key paths, privacy grants and anything credential-shaped. Read one before
|
|
130
|
+
you send it anywhere.
|
|
131
|
+
|
|
132
|
+
## Let an AI assistant triage the report
|
|
133
|
+
|
|
134
|
+
[`SKILL.md`](SKILL.md) is a self-contained skill that runs the audit, reads the
|
|
135
|
+
dataset and returns a short ranked list of fixes rather than eighty raw
|
|
136
|
+
findings. For Claude Code, copy it to `~/.claude/skills/macverify-review/`; for
|
|
137
|
+
Codex, append it to your `AGENTS.md`. It never runs a destructive command
|
|
138
|
+
without asking, and never runs `sudo` on your behalf.
|
|
139
|
+
|
|
140
|
+
## Requirements
|
|
141
|
+
|
|
142
|
+
macOS 11+ and Python 3.9+; every Mac since macOS 12.3 ships a suitable
|
|
143
|
+
`python3`. No third-party packages, at install time or runtime.
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT. See [LICENSE](LICENSE). Also [CONTRIBUTING.md](CONTRIBUTING.md),
|
|
148
|
+
[CHANGELOG.md](CHANGELOG.md), [SECURITY.md](SECURITY.md) and
|
|
149
|
+
[design_notes.md](design_notes.md).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
DISTRIBUTION = "macverify"
|
|
5
|
+
|
|
6
|
+
UNKNOWN_VERSION = "0+unknown"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _installed_version():
|
|
10
|
+
try:
|
|
11
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
12
|
+
except ImportError:
|
|
13
|
+
return None
|
|
14
|
+
try:
|
|
15
|
+
return version(DISTRIBUTION)
|
|
16
|
+
except PackageNotFoundError:
|
|
17
|
+
return None
|
|
18
|
+
except Exception:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _source_version():
|
|
23
|
+
path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "pyproject.toml")
|
|
24
|
+
try:
|
|
25
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
26
|
+
for line in handle:
|
|
27
|
+
match = re.match(r'^\s*version\s*=\s*["\']([^"\']+)["\']\s*$', line)
|
|
28
|
+
if match:
|
|
29
|
+
return match.group(1)
|
|
30
|
+
except (OSError, ValueError):
|
|
31
|
+
return None
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _resolve_version():
|
|
36
|
+
return _installed_version() or _source_version() or UNKNOWN_VERSION
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__version__ = _resolve_version()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from . import fsutil
|
|
5
|
+
|
|
6
|
+
STOPWORDS = {
|
|
7
|
+
"the", "a", "an", "and", "or", "for", "with", "when", "use", "using", "used", "this", "that", "to", "of", "in",
|
|
8
|
+
"on", "by", "is", "are", "be", "it", "its", "as", "at", "from", "into", "you", "your", "should", "can", "will",
|
|
9
|
+
"any", "all", "not", "but", "if", "then", "than", "also", "via", "per", "up", "out", "over", "before", "after",
|
|
10
|
+
"skill", "skills", "agent", "agents", "command", "commands", "claude", "code", "user", "users", "need", "needs",
|
|
11
|
+
"have", "has", "was", "were", "there", "their", "them", "they", "what", "which", "who", "how", "why", "more",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
NOT_APPLICABLE = "n/a"
|
|
15
|
+
|
|
16
|
+
MAX_ANCESTOR_DEPTH = 12
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def tokens(text):
|
|
20
|
+
words = re.findall(r"[a-z][a-z0-9_-]{2,}", (text or "").lower())
|
|
21
|
+
return {word for word in words if word not in STOPWORDS}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def similarity(left, right):
|
|
25
|
+
if not left or not right:
|
|
26
|
+
return 0.0, []
|
|
27
|
+
shared = left & right
|
|
28
|
+
union = left | right
|
|
29
|
+
if not union:
|
|
30
|
+
return 0.0, []
|
|
31
|
+
return round(len(shared) / float(len(union)), 3), sorted(shared)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def cost(always_bytes, on_demand_bytes):
|
|
35
|
+
return {
|
|
36
|
+
"always_loaded_bytes": always_bytes,
|
|
37
|
+
"always_loaded_tokens_estimate": always_bytes // 4,
|
|
38
|
+
"on_demand_bytes": on_demand_bytes,
|
|
39
|
+
"on_demand_tokens_estimate": on_demand_bytes // 4,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def no_cost(reason):
|
|
44
|
+
return {
|
|
45
|
+
"always_loaded_bytes": None,
|
|
46
|
+
"always_loaded_tokens_estimate": NOT_APPLICABLE,
|
|
47
|
+
"on_demand_bytes": None,
|
|
48
|
+
"on_demand_tokens_estimate": NOT_APPLICABLE,
|
|
49
|
+
"measurement_note": reason,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def headings(body, limit=40):
|
|
54
|
+
outline = []
|
|
55
|
+
for line in (body or "").splitlines():
|
|
56
|
+
match = re.match(r"^(#{1,6})\s+(.+?)\s*#*$", line)
|
|
57
|
+
if match:
|
|
58
|
+
outline.append({"level": len(match.group(1)), "text": match.group(2)[:120]})
|
|
59
|
+
if len(outline) >= limit:
|
|
60
|
+
break
|
|
61
|
+
return outline
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def ancestors(start, home, include_home=True):
|
|
65
|
+
chain = []
|
|
66
|
+
current = os.path.abspath(start)
|
|
67
|
+
while True:
|
|
68
|
+
chain.append(current)
|
|
69
|
+
parent = os.path.dirname(current)
|
|
70
|
+
if parent == current or len(chain) >= MAX_ANCESTOR_DEPTH:
|
|
71
|
+
break
|
|
72
|
+
if current == home:
|
|
73
|
+
break
|
|
74
|
+
current = parent
|
|
75
|
+
if include_home and home not in chain and fsutil.exists(home):
|
|
76
|
+
chain.append(home)
|
|
77
|
+
return chain
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def project_roots(ctx):
|
|
81
|
+
roots = []
|
|
82
|
+
for root in [ctx.cwd] + list(ctx.projects):
|
|
83
|
+
absolute = os.path.abspath(os.path.expanduser(root))
|
|
84
|
+
if absolute not in roots:
|
|
85
|
+
roots.append(absolute)
|
|
86
|
+
return roots
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def search_roots(ctx):
|
|
90
|
+
home = fsutil.home()
|
|
91
|
+
seen = []
|
|
92
|
+
for root in project_roots(ctx):
|
|
93
|
+
for candidate in ancestors(root, home):
|
|
94
|
+
if candidate not in seen:
|
|
95
|
+
seen.append(candidate)
|
|
96
|
+
return seen
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def quote_lines(text, terms, limit=2, width=160):
|
|
100
|
+
if not text or not terms:
|
|
101
|
+
return []
|
|
102
|
+
wanted = set(terms)
|
|
103
|
+
picked = []
|
|
104
|
+
for line in text.splitlines():
|
|
105
|
+
stripped = line.strip().lstrip("#").strip()
|
|
106
|
+
if len(stripped) < 20:
|
|
107
|
+
continue
|
|
108
|
+
if tokens(stripped) & wanted:
|
|
109
|
+
picked.append(stripped[:width])
|
|
110
|
+
if len(picked) >= limit:
|
|
111
|
+
break
|
|
112
|
+
return picked
|