Skip to content

Repository files navigation

English · Русский

gitmon — git-status intrusion monitor

A small, auditable monitor that detects unauthorized file changes in your web applications and alerts you via Telegram, MAX and email. It runs git status against each tracked working tree on a schedule and reports only genuinely new deviations — the same signal that catches dropped web shells (untracked ?? evil.php) and injected files (modified vendor/composer/autoload_real.php).

Pure Python 3 + coreutils, no external packages. Designed to stay quiet on noisy repos and to resist the obvious evasion tricks.

gitmon demo — detects a freshly dropped shell, alerts Telegram/MAX/email, then stays quiet on the next run (delta)


Origin

A production web server (a Joomla site, some legacy PHP, a couple of Laravel apps) was breached through a vulnerable Joomla component. The attacker dropped a zoo of web-shell families and, worse, a re-infector welded into Composer's vendor/composer/autoload_real.php that ran on every request and kept re-dropping file-manager shells into surname-named dirs, each with its own PHP-re-enabling .htaccess — delete a shell, refresh the page, it's back.

What actually surfaced the breach was nothing fancy: git status. The code lived in git, so the attack showed up as a wall of untracked entries (?? — the dropped shells) and modified tracked files ( M — the injected index.php and autoload_real.php). The git baseline already knew what belonged; everything else was the attack, laid out plainly. That one diagnostic, run once, found the compromise — so why not run it continuously and have it message you the moment a new deviation appears? That is gitmon.


How it compares

vs file-integrity monitors (AIDE, Tripwire, inotify FIM)

AIDE / Tripwire inotify FIM gitmon
Knows legit-vs-unexpected builds its own DB of every file watches paths, no notion of "expected" reuses the git baseline — legit code is known for free
Noise on big upload dirs high (every upload is a "change") high delta-quiet — baseline absorbs known untracked files
Injected tracked code yes (checksum) yes (write event) yes ( M) + names the high-value files
Commit-to-clean evasion n/a n/a caught via HEAD tracking
Hide-by-.gitignore evasion n/a n/a caught via .gitignore hashing
Setup define policy, init DB configure watches point at repos that already exist
Alerting log / email (varies) DIY Telegram + MAX + email built in
Dependencies package + DB inotify tooling Python 3 stdlib, no DB, no agent
Dead-man switch external external built-in watchdog

FIM tools are powerful but have to learn your filesystem from scratch and don't understand "this 400k-file uploads folder is normal." gitmon starts from the answer your repo already encodes.

vs CMS security plugins (Wordfence, Sucuri, Joomla extensions)

CMS plugins gitmon
Where it runs inside the web app (itself attackable) outside the webroot, server-side
Scope one CMS install all repos / all sites on the box at once
CMS-agnostic no (per-platform) yes — Joomla, WordPress, Laravel, plain PHP
Survives app compromise not necessarily yes — separate process, config outside webroot

A plugin lives inside the thing being attacked. gitmon watches from the outside, across every site.


Why git status?

After a breach, attackers drop shells and inject code into the document root. In a git-managed site these show up immediately as untracked (??) or modified ( M) entries — exactly what git status surfaces. gitmon turns that one-off forensic trick into continuous monitoring.


How it works

  • Discovers + dedups the configured git working trees (by realpath).
  • Per repo runs git status --porcelain=v1 --untracked-files=all -z (NUL-safe parsing, robust to spaces/newlines/UTF-8 in filenames), plus the current HEAD and the hashes of tracked .gitignore files.
  • Delta alerting: the first run silently records a baseline; afterwards it alerts only on records that are new vs the baseline. This is essential — a real site can have hundreds of thousands of untracked uploads; you must not alert on all of them, only on what newly appears.
  • Severity
    • HIGH — a new file with an executable-PHP extension (.php .phtml .php5 .pht .suspected .htaccess), a tracked code file modified/added/deleted, force-HIGH IOCs (vendor/composer/autoload_real.php, any index.php, .htaccess, .gitignore change).
    • INFO — everything else (normal media uploads .jpg/.png/.gif, docs, etc.). With notify_level = HIGH (default) INFO is logged and folded into the baseline but not paged.
  • Content second opinion (optional): new untracked media/text files are passed to a content scanner (scan-webshells). A shell disguised as an image (GIF89a<?php …) is promoted to HIGH by content, even though its extension looks benign.
  • Anti-evasion
    • HEAD tracking — catches an attacker who commits the shell to make the working tree clean (a clean fast-forward on the same branch is INFO; non-fast-forward / suspicious files in the diff are HIGH).
    • .gitignore hashing — catches "hide the file by adding it to .gitignore".
  • Notifications: one aggregated message per run, sent only when there are new findings, to Telegram + MAX + email. Tokens never appear in process arguments (sent via urllib in-process).
  • Watchdog (dead-man switch): a separate cron job alerts if gitmon stops running or if alert delivery has been failing — so silencing the monitor is itself noticed.

Install

# as root, from a checkout of this repo:
./install.sh
# creates /root/git-watch (700), installs the scripts, the cron jobs and a logrotate rule,
# then silently seeds the baselines.

Then edit the config and list your repos:

nano /root/git-watch/gitmon.conf     # tokens, recipients, [repos]

Cron runs gitmon every 10 minutes and the watchdog every 30.


Configure

See gitmon.conf. Key fields:

Section Field Meaning
[telegram] token / chat_id bot token (@BotFather) + target chat (empty = Telegram off)
api_base default api.telegram.org; set to a reverse-proxy if Telegram is blocked
[max] token / chat_id MAX bot token + admin user_id (empty = MAX off)
[email] recipients / from sent via local sendmail
[options] notify_level HIGH (quiet) or INFO (page everything)
force_root_git 0 = run git as repo owner (sudo), 1 = as root
use_scanner / scanner_path enable the content second opinion
[prune] patterns substrings to ignore for untracked noise (never affects tracked code)
[repos] label = /path one git working tree per line

Run modes

/root/git-watch/run.sh            # normal run (seed if no baseline, else alert on delta, then fold)
/root/git-watch/run.sh --dry      # show findings + what WOULD be sent; change nothing, send nothing
/root/git-watch/run.sh --seed     # (re)seed baselines silently
cat /root/git-watch/gitmon.log    # history

Security model

  • Script and config live outside the web root, root-owned; the config is chmod 600 so an attacker with web-server-level access cannot read the bot tokens.
  • Bot tokens are passed to the HTTP layer in-process (urllib), never as command arguments, so they don't appear in the process list.
  • State (baselines) is root-owned; the cron and watchdog are root jobs.

Limitations

  • Monitors git working trees only. Files dropped outside any repo (e.g. a non-git uploads area) are not seen by git status — pair gitmon with a content scanner sweep for those.
  • The signature/severity rules are tuned for PHP web stacks; adjust CODE_EXT / [prune] / the scanner for your environment.

License

MIT — see LICENSE.

About

git-status intrusion monitor for web servers — detects dropped shells / code injections, alerts via Telegram, MAX and email

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages