A lightweight, open-source security monitor for macOS developers. Jaya-defender watches your running processes and credential files in real-time, alerting you the moment something looks like a crypto miner, a credential thief, or a living-off-the-land malware binary — with near-zero CPU and memory overhead.
Built specifically for developers on Mac Minis and Apple Silicon machines who run compute-heavy workloads and store SSH keys, AWS credentials, and API tokens locally.
Developer machines are high-value targets. A compromised Mac Mini can:
- Run a crypto miner in the background, silently consuming your CPU and electricity for weeks
- Exfiltrate your SSH private keys, AWS credentials, and
.envfiles to a remote server - Use macOS system binary names (
accountsd,launchd) to hide in plain sight inpsoutput
Standard antivirus tools are noisy, resource-hungry, and miss the patterns that matter most to developers. Jaya-defender is surgical: it knows what a dev machine looks like at rest and flags only what's genuinely anomalous.
Requirements: macOS 12+, Python 3.10+
# 1. Clone
git clone https://github.com/Ojaswy/jaya.git
cd jaya-defender
# 2. Install (creates ~/.jaya/ and registers a launchd daemon)
chmod +x install.sh && ./install.sh
# 3. Verify it's running
launchctl list | grep jaya
tail -f ~/.jaya/jaya.logThe daemon starts immediately and survives reboots via launchd. Alerts appear as macOS Notification Center banners and are written to ~/.jaya/jaya.log.
pip install -e ".[dev]"
python -m jayapython -m pytest tests/ -v
# Expected: 55 passedJaya-defender runs two monitors in parallel:
Process sensor — polls all running processes every 3 seconds using psutil (wraps the macOS libproc API). For each process it checks: executable path, name, open network connections, open file handles, and sustained CPU usage. A bounded queue carries findings to the alert dispatcher without blocking the sensor thread.
File watcher — uses watchdog/FSEvents (Apple's native kernel-level file-change notification) to receive instant callbacks when your credential directories (~/.ssh, ~/.aws, ~/.gnupg, ~/.config/gcloud) are modified. Zero polling overhead.
Both monitors feed into a pure rule engine — nine stateless functions that each take a process dict and return an optional finding. No global state, fully unit-tested.
| # | Rule | Severity | Description |
|---|---|---|---|
| 1 | KNOWN_MINER_BINARY |
HIGH | Executable name matches a known crypto miner (xmrig, ethminer, cgminer, nbminer, and 12 others) |
| 2 | STRATUM_MINING |
HIGH | Established TCP connection to a known stratum mining port (3333, 4444, 5555, 7777, 9999, and 5 others) |
| 3 | CRYPTO_MINER_CPU |
MEDIUM |
Process sustains >80% CPU for 5 consecutive scans (15 seconds). Asks for confirmation — could be ML training or video export |
| 4 | CREDENTIAL_READ |
HIGH | A non-whitelisted process has an SSH key, AWS credentials file, or GnuPG private key open |
| 5 | SUSPICIOUS_SPAWN_PATH |
MEDIUM |
Executable launched from /tmp/, /private/tmp/, or /var/folders/. Asks for confirmation |
| 6 | DOWNLOADS_EXECUTABLE |
LOW | Binary running from ~/Downloads |
| 7 | HIDDEN_DIR_EXECUTABLE |
HIGH | Executable path contains a hidden (.) directory anywhere inside your home folder (e.g. ~/.com.apple.accountsd/payload) |
| 8 | LIBRARY_EXECUTABLE |
HIGH | Standalone binary in ~/Library/Application Support, ~/Library/Caches, or ~/Library/Containers that is not inside a .app bundle |
| 9 | SYSTEM_NAME_SPOOF |
HIGH | Process name matches a macOS system daemon (accountsd, launchd, securityd, etc.) but runs from a user-writable path |
- HIGH — Instant Notification Center banner, logged, no user interaction needed. Near-zero false positive rate.
- MEDIUM — Interactive AppleScript dialog (60-second timeout). Click Mark Safe to permanently whitelist the executable for that rule. Click Investigate to open the log in Console.app. Timeout = logged only.
- LOW — Logged and printed to terminal. No notification.
Rules 1, 2, and 3 together cover the full attack surface: known binary names, network fingerprints (stratum protocol), and anomalous CPU usage. Rules 7, 8, and 9 catch custom miners that rename themselves after system processes and drop into ~/Library.
Rule 4 fires when a non-whitelisted process has a credential file open during a polling scan. This catches threats that hold the file open for more than the 3-second poll interval — for example, a process that reads, encodes, and uploads your SSH key over a slow connection, or a C2 implant that keeps the keyring open between commands.
What Rule 4 does not catch: a process that reads ~/.ssh/id_rsa, encodes it, exfiltrates it over HTTPS to port 443, and exits — all within 3 seconds. That pattern requires kernel-level interception (Apple's Endpoint Security framework) which jaya-defender does not currently use. See Limitations for details.
Real-world macOS malware increasingly avoids custom binaries and instead hides its payloads in paths like ~/Library/Application Support/.com.apple.accountsd/AccountsHelper. This binary has a plausible-sounding name, runs from a data directory, and shows up in ps looking like a system process. Rules 7, 8, and 9 are designed specifically to catch this pattern without relying on any name or port match.
Rules 5 and 6 provide low-friction coverage of common dropper paths. If something was downloaded and is now executing, you'll know.
On first run, jaya-defender writes default configuration to ~/.jaya/config.json. Edit that file to tune any threshold or list:
{
"cpu_high_threshold": 80.0,
"cpu_sustained_samples": 5,
"stratum_ports": [3333, 4444, 5555, 7777, 9999, 14444, 45560, 3032, 8008, 14433],
"miner_name_patterns": ["xmrig", "ethminer", "cgminer", "..."],
"credential_reader_whitelist": ["ssh", "git", "aws", "Code", "Cursor", "..."],
"extra_dev_dotdirs": [],
"extra_library_exe_whitelist": [],
"alert_cooldown_secs": 300,
"log_dir": "/Users/you/.jaya"
}extra_dev_dotdirs — Add hidden directory names that your tooling creates and should not be flagged by HIDDEN_DIR_EXECUTABLE. For example, if you use Deno or Bun:
"extra_dev_dotdirs": [".deno", ".bun", ".uv"]The built-in list already covers .venv, .cargo, .tox, .nox, .npm, .yarn, .pnpm, .git, .pyenv, .rbenv, .rustup, .gradle, .volta, .direnv, .nodenv, and .m2.
extra_library_exe_whitelist — Add binary basenames that legitimately live as standalone executables inside ~/Library without a .app bundle. The built-in list already covers 18 common LSP daemons (rust-analyzer, clangd, pyright-langserver, gopls, pylsp, typescript-language-server, copilot-language-server, and more). Use this for site-specific tools:
"extra_library_exe_whitelist": ["my-internal-tool"]credential_reader_whitelist — If a legitimate tool in your workflow reads credential files and you want to stop seeing CREDENTIAL_READ alerts for it, add its executable basename here.
alert_cooldown_secs — The same (pid, rule) pair will not re-alert within this window. Default: 5 minutes.
When you click Mark Safe on a MEDIUM dialog, the executable path is written to ~/.jaya/whitelist.json. That rule will never fire for that binary again, even after daemon restarts. You can inspect or edit the file directly:
{
"CRYPTO_MINER_CPU": ["/usr/local/bin/stable-diffusion-runner"],
"SUSPICIOUS_SPAWN_PATH": ["/var/folders/xx/.../my-known-tool"]
}Jaya-defender is built on macOS-native APIs (FSEvents via watchdog, osascript for notifications, launchd for auto-start). It will not run on Linux or Windows.
Jaya-defender uses psutil, a userspace library. It can only see what the OS exposes to unprivileged processes via periodic snapshots. It cannot:
- Intercept individual file reads or system calls as they happen
- Detect a process that reads a credential file and exits in under 3 seconds
- Observe encrypted network traffic content (only port numbers and connection state)
- Detect processes that hide themselves from
proc_pidinfo
The practical consequence: fast exfiltration is not covered. An attacker-controlled process can read ~/.ssh/id_rsa, base64-encode it, POST it to a server over HTTPS on port 443, and exit — all in under 1 second. Jaya-defender will not see any of these three events. Covering this threat model requires Apple's Endpoint Security framework (requires a notarised entitlement), which is planned for a future version.
What Rule 4 does catch: processes that hold a credential file open across multiple poll intervals — for example, an implant keeping the keyring open between C2 check-ins, or a slow uploader that takes longer than 3 seconds to complete.
A process running as root (uid=0) is skipped for open_files inspection to avoid noise and speed up scanning. Root-level persistent credential access is not covered by Rule 4.
The process sensor polls every 3 seconds. A process that launches, completes its work, and exits in under 3 seconds may not be observed. This is an acceptable trade-off for the target threat model (miners and stealers run continuously), but it means jaya-defender is not suited for detecting short-lived payloads.
CRYPTO_MINER_CPU (Rule 3) fires on any process sustaining >80% CPU for 15 seconds. On a developer machine running ML training, video encoding, or compilation, this will trigger. This is intentional — the MEDIUM severity and interactive confirmation dialog let you mark your known-heavy processes as safe once and never see the alert again.
The built-in _LSP_BASENAMES whitelist for LIBRARY_EXECUTABLE covers the 18 most common language server daemons. New LSP servers (e.g. zls for Zig, kotlin-language-server) are not automatically detected. Use extra_library_exe_whitelist in your config until the built-in list is updated.
The CPU threshold is a fixed value, not adaptive. A machine that routinely pegs one core at 90% for legitimate work will produce repeated MEDIUM alerts until those processes are whitelisted. Phase 2 will introduce an EMA (exponential moving average) behavioral baseline per process, which will allow the rule to fire only when CPU usage deviates significantly from that process's historical norm.
High-severity findings are reported as raw facts (path, rule, pid). Phase 3 will optionally connect to a local Ollama model to generate a human-readable attack chain narrative (e.g. "This appears to be a persistence payload disguised as a system daemon — here is why and what to do next").
The MEDIUM confirmation dialogs use osascript and require an active desktop session. On headless Mac Minis accessed only over SSH, the dialog will silently time out and the finding will be logged only. Set cpu_sustained_samples higher or pre-populate your whitelist if running headless.
Jaya-defender is detection-only. It logs, notifies, and asks questions — it never kills processes, deletes files, or blocks network connections. Automated response is intentionally out of scope to avoid false-positive damage on developer machines.
jaya-defender/
├── jaya/
│ ├── __init__.py # Package version
│ ├── config.py # Config dataclass, JSON load/save
│ ├── rules.py # 9 pure detection rule functions
│ ├── sensor.py # psutil process sensor thread
│ ├── watcher.py # watchdog/FSEvents file watcher
│ ├── alert.py # Alert dispatcher, confirmation dialogs, whitelist
│ ├── daemon.py # Main event loop, SIGTERM handler
│ └── __main__.py # python -m jaya entry point
├── tests/
│ └── test_rules.py # 55 pure unit tests (no OS calls)
├── install.sh # One-shot install + launchd plist registration
├── pyproject.toml
└── uninstall.sh
| Path | Contents |
|---|---|
~/.jaya/jaya.log |
JSON-lines log of every finding with timestamp |
~/.jaya/whitelist.json |
Per-rule exe paths the user has marked safe |
~/.jaya/config.json |
User configuration (created on first run) |
Phase 2 — Behavioral baseline (planned)
Replace the fixed CPU threshold in CRYPTO_MINER_CPU with a per-process EMA that adapts to each machine's workload. A process that normally runs at 85% CPU will not trigger; a new process pegging at 90% on a machine that's usually idle at 5% will. Correlate with absence of expected network activity to distinguish miners from ML jobs.
Phase 3 — Local LLM analyst (planned) Optional integration with a locally-running Ollama model. When a HIGH finding fires, the LLM receives the full finding context (process tree, path, rule, open files) and produces a human-readable narrative explaining the likely attack chain and recommended response steps. No data leaves the machine.
v5 config audit (planned)
Expose _SYSTEM_NAMES (the spoofed daemon name list) and stratum_ports as user-editable lists in config.json, using the same extension pattern as extra_dev_dotdirs.
Pull requests welcome. Before submitting:
- Add or update tests in
tests/test_rules.py— all rules must be tested with pure process dicts (nopsutil, no OS calls) - Run
python -m pytest tests/ -vand confirm all tests pass - If adding a new detection rule, add an entry to the rules table in this README and document the threat model it addresses
- If modifying
config.py, update the configuration section above
MIT