Skip to content
EpicMorgPublic

About

You describe your services once. Enodia handles the rest.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

What it does

          ░░
         ▒██▒
        ▓████▓
  ▓    ▓██████▓    ▓
 ▓█▓░  ▒██████▒  ░▓█▓
 ██████▓▒▓██▓▒▓██████
 ▒███████▓▒▒▓███████▒
  ░▒▓█████░░█████▓▒░
        ▒████▒
          ██
          ▒▒

█▀▀ █▄ █ █▀█ █▀▄ █ ▄▀█
██▄ █ ▀█ █▄█ █▄▀ █ █▀█ asks your deployed services what version they are running, then checks

those versions against vendor lifecycle calendars and tells you what is
already dead, what is dying, and where your fleet has drifted apart.

You describe your services once. Enodia handles the rest.

Know what you are running, and how long it has left.

Activity GitHub issues GitHub forks GitHub stars Size Release License: AGPL v3

docs.enodia.sh · full docs, config reference, and per-product probe notes.

Status: 2.2. The full pipeline (collect → inventory → evaluate → render), 123 probes, CVE correlation against BDU ФСТЭК, NVD and vendors' own advisories and, for twelve Linux distributions, per installed package, enodia cve update for the databases, settings.yaml, and the release/packaging pipeline are all implemented and used against real production infrastructure. See CHANGELOG.md for release history, or the latest release for downloads.


Example:

schemaVersion: 1
targets:
  - id: jira-main
    product: jira
    address: https://jira.example.com

  - id: gitlab-main
    product: gitlab
    address: https://gitlab.example.com
    credentials: gitlab-token

  - id: web01
    product: debian
    address: web01.example.com
    credentials: web01-ssh

credentials:
  gitlab-token:
    kind: token-header
    header: PRIVATE-TOKEN
    value: "${GITLAB_TOKEN}"

  web01-ssh:
    kind: ssh-key
    username: enodia
    private_key_file: /etc/enodia/keys/web01
$ enodia check
ID           PRODUCT  PATCH   LIFECYCLE  BRANCH     SEVERITY  REASON  CVES
jira-main    jira     behind  active     newer_lts  warn      -       -
gitlab-main  gitlab   behind  eol        newer      fail      -       -
web01        debian   current active     latest     ok        -       -

Save this as enodia.yaml next to the binary — see "File locations" below for every name and directory enodia actually searches, and the exact order.

Why not something else

Tool Tracks upstream releases Knows what you deployed Lifecycle dates
eol CLI — — yes
nvchecker yes — —
Renovate / Dependabot yes (dependencies) — —
what's-up-docker yes (containers only) containers only —
Uptime Kuma — up/down only —
Enodia yes yes yes

The gap Enodia fills is the join: inventory of the live fleet, matched against lifecycle data.

Design

Two phases, deliberately separable. Collection talks to your services; evaluation talks to the internet. They almost never have network access to the same places.

collect  →  inventory.jsonl  →  evaluate  →  assessment  →  render

For air-gapped environments this is the whole point:

# inside the closed network — no internet needed
enodia collect --config config.yaml -o inventory.jsonl

# anywhere else — no access to your services needed
enodia check --from inventory.jsonl

Three orthogonal axes, not one verdict. A branch can be perfectly healthy while a newer major exists — Confluence 10 LTS is alive and supported even though 11 shipped. Collapsing that into a single status throws away the information you actually wanted.

Axis Values
Patch current · behind · ahead · unknown
Lifecycle active · security · eol · unknown
Newer branch latest · newer · newer_lts · unknown

Facts and judgement are separate. The inventory records what was observed. Severity is computed on top, from policy you control. Export the facts and apply your own rules if ours do not fit.

Time is a parameter. Evaluation never calls the system clock itself — it always takes asOf explicitly, sourced from the inventory's own collection timestamp (inventory.jsonl's collectedAt). Re-running check --from an old inventory evaluates it as of when it was collected, not today, so results and tests both stay deterministic instead of silently drifting with the calendar.

Probes are compiled in. One product, one file, one entry in an explicit registry. Adding support means a new release, not a plugin ABI. For anything in-house, product: generic takes a parser spec straight from your config.

Transport isn't assumed to be HTTP. Most probes speak HTTP, but Redis, PostgreSQL, MySQL and MongoDB speak their own wire protocols directly, and a growing set — every Linux distro, the BSDs, macOS, OPNsense — is reached over SSH instead: enodia runs one identifying command (cat /etc/os-release, sw_vers, ...) and reads the answer, the same way an HTTP probe reads a JSON body. kind: ssh-key and kind: password credentials cover both auth shapes; host key verification reuses the same pin_sha256/insecure fields TLS targets already have.

A credential's kind has to match its transport. HTTP products read basic, bearer or token-header; SSH probes read ssh-key or password; Redis, PostgreSQL and the other wire-protocol probes read password. A user/password pair for a web UI or REST API (RouterOS, Harbor, Jenkins, a BMC) is therefore kind: basic, not kind: password. config validate and every run reject a mismatch and name the kinds the product does accept (D48).

Views

check and export both render one of four focuses, picked with --view (or settings.yaml's render.default_view when the flag isn't passed — see "Reporting" below). Each view is a different slice of the same inventory + assessment data, not a different data source.

compact (the default) — one row per target: the three axes, overall severity, and why, if anything needs attention. This is what plain enodia check shows, as in the example above.

lifecycle — when each target's lifecycle actually ends:

$ enodia check --from inventory.jsonl --view lifecycle
ID           PRODUCT  LIFECYCLE  EOL         SUPPORT-ENDS  DAYS-TO-EOL
jira-main    jira     active     2026-12-05  -             338
gitlab-main  gitlab   eol        2025-01-16  2024-11-21    -350

drift — installed version against the latest release in the same cycle:

$ enodia check --from inventory.jsonl --view drift
ID           PRODUCT  CURRENT  LATEST   CYCLE  PATCH   CVES
jira-main    jira     10.3.1   10.3.25  10.3   behind  5
gitlab-main  gitlab   17.5.0   17.5.5   17.5   behind  255

fleet — version spread and reachability across every instance of a product, grouped instead of listed one row per target. The offline-only view: it needs nothing but the inventory itself, no lifecycle resolver, no internet access at all. Two failed instances of the same product with different failure kinds (auth vs. unreachable) get their own rows, not a shared (unknown) bucket:

$ enodia check --from inventory.jsonl --view fleet
PRODUCT  VERSION    STATUS       COUNT  INSTANCES
gitlab   (unknown)  auth         1      gitlab-2
gitlab   18.2.1     ok           1      gitlab-1
jira     (unknown)  unreachable  1      jira-staging
jira     10.3.1     ok           1      jira-3
jira     10.3.2     ok           2      jira-1, jira-2

export --format json/prometheus ignore --view entirely — they always carry every observation and assessment; views only shape the table and the HTML report (see "Reporting").

Installation

Each release carries a .deb, .rpm, .apk and Arch's .pkg.tar.zst (linux/amd64+arm64) alongside the raw archives, plus an official container image:

sudo dpkg -i enodia_linux_amd64.deb                # Debian/Ubuntu
sudo rpm -i enodia_linux_amd64.rpm                 # Fedora/RHEL
apk add --allow-untrusted enodia_linux_amd64.apk   # Alpine
sudo pacman -U enodia_linux_amd64.pkg.tar.zst      # Arch

docker run --rm \
  -v /etc/enodia:/config:ro \
  ghcr.io/epicmorg/enodia:1 check --config /config/config.yaml

The same image is also published to docker.io/epicmorg/enodia and Quay — same tags (latest, 1, and the exact version, e.g. 1.0.0-0), pick whichever registry you already pull from. Its Dockerfile lives outside this repo, in the EpicMorg/docker monorepo alongside the house images and several other products this project's own probes cover — still the official image, just built and published on that repo's own schedule rather than in this repo's release pipeline (see docs/DECISIONS.md D17).

All four packages install the binary at /usr/bin/enodia, man pages for every command under /usr/share/man/man1/ (man enodia, man enodia-collect, ...), and create a dedicated, unprivileged enodia system user — nothing in this package needs root to run, so enodia serve under systemd shouldn't get any more than it does. Three empty directories are created and chowned to that user: /etc/enodia (root:enodia, 0750 — readable by the service via group membership, not writable by it — for your enodia.yaml/settings.yaml/credentials.yaml; the package never writes a config into it, a missing one is meant to be a loud error), and /opt/enodia//var/enodia (enodia:enodia, 0750) for whatever the service writes at runtime.

Or, for the raw archive rather than a package:

curl -sSL https://raw.githubusercontent.com/EpicMorg/enodia/master/install.sh | sh   # Linux/macOS
irm https://raw.githubusercontent.com/EpicMorg/enodia/master/install.ps1 | iex        # Windows

Supported platforms

OS Arch Minimum version
Linux amd64, arm64 Kernel 3.2 or later — Debian 8+, Ubuntu 14.04+, RHEL/CentOS 7+ all comfortably qualify
Windows amd64, arm64, 386 Windows 10 / Windows Server 2016 or later
macOS amd64, arm64 macOS 12 Monterey or later
Android (Termux) arm64 only Android 7+ (Termux's own floor) — a separate enodia_android_arm64 build, not the Linux one (see docs/DECISIONS.md D20); amd64/386/arm aren't built yet (D20/ROADMAP). Rooted devices (Magisk/KernelSU) may need su to run it at all, due to an open upstream bug, termux-exec#40 — not something fixable from this side, see D20

These are the Go 1.26 toolchain's own floor (confirmed against go.dev/wiki/MinimumRequirements directly, not assumed), not something enodia adds on top — building from source with a newer Go raises the macOS floor further (1.27 requires macOS 13 Ventura), since that's a toolchain decision, not a project one. CGO_ENABLED=0 (see .goreleaser.yaml) means the binary is fully static and never links libc at all: on Linux, only the kernel version matters, not which distro or glibc version is underneath.

Reporting

enodia export --format html writes a single self-contained file. Point nginx at it and refresh it from cron or a systemd timer.

There is no built-in web server and no refresh button, by design: a button that polls your entire fleet on every click is a self-inflicted denial of service. Collection runs on a schedule; the page shows the latest snapshot and states plainly when it was taken.

An optional settings.yaml holds personal display defaults: which format export uses when --format isn't passed, which table view check/export use when --view isn't passed, and how export --format html renders — see "File locations" below for every name and directory it's searched in, and why a missing one is never an error the way a missing enodia.yaml is. For example:

schemaVersion: 1

export:
  # used whenever `export` is run without --format; the built-in default
  # stays json either way
  default_format: html

render:
  # compact (default) | lifecycle | drift | fleet
  default_view: fleet

html:
  # inline (default, fully offline) | cdn (loads Bootstrap/Bootswatch)
  assets: cdn

  # none (no stylesheet at all) | default (plain Bootstrap) | any of
  # Bootswatch's 26 real themes: brite, cerulean, cosmo, cyborg, darkly,
  # flatly, journal, litera, lumen, lux, materia, minty, morph, pulse,
  # quartz, sandstone, simplex, sketchy, slate, solar, spacelab,
  # superhero, united, vapor, yeti, zephyr
  theme: lumen

  # auto (default: races jsdelivr and cdnjs, uses whichever answers
  # first) | jsdelivr | cdnjs
  cdn: auto

  # optional: restrict the export to one view instead of all four
  # view: fleet

Every html.* field here only matters for export --format html; the default enodia check table is unaffected by any of them except render.default_view. See docs/DECISIONS.md D19 for the full reasoning, including why a corrupted or unrecognised theme saved in a viewer's browser resets to this file's html.theme, not to some hardcoded name.

Row colors in CDN mode

With html.assets: cdn, export --format html gives each row a Bootstrap contextual class — red for a failed instance, green for a reachable one (see "Views" above for the plain data), in whatever Bootswatch theme is configured, not a hardcoded color enodia has to maintain per theme. Here's the fleet view's rows from the same data:

<table class="table table-striped table-hover table-sm align-middle">
<thead><tr><th>PRODUCT</th><th>VERSION</th><th>STATUS</th><th>COUNT</th><th>INSTANCES</th></tr></thead>
<tbody>
<tr class="table-danger"><td>gitlab</td><td>(unknown)</td><td>auth</td><td>1</td><td>gitlab-2</td></tr>
<tr class="table-success"><td>gitlab</td><td>18.2.1</td><td>ok</td><td>1</td><td>gitlab-1</td></tr>
<tr class="table-danger"><td>jira</td><td>(unknown)</td><td>unreachable</td><td>1</td><td>jira-staging</td></tr>
<tr class="table-success"><td>jira</td><td>10.3.1</td><td>ok</td><td>1</td><td>jira-3</td></tr>
<tr class="table-success"><td>jira</td><td>10.3.2</td><td>ok</td><td>2</td><td>jira-1, jira-2</td></tr>
</tbody>
</table>

CVE correlation

Optional. enodia can match every probed version against local vulnerability databases: BDU ФСТЭК and NIST NVD, distributions' own security data and some vendors' own. Point enodia.yaml at the files; enodia cve update downloads them, or fetch them yourself. check, collect and serve never download anything.

cve:
  bdu:
    path: /var/lib/enodia/cve/bdu/vulxml.zip   # https://bdu.fstec.ru/files/documents/vulxml.zip
  nvd:
    path: /var/lib/enodia/cve/nvd              # a directory of nvdcve-2.0-<year>.json.gz
  debian:
    path: /var/lib/enodia/cve/debian.json      # https://security-tracker.debian.org/tracker/data/json
  oval:
    path: /var/lib/enodia/cve/oval             # vendor OVAL files, one per release (see below)
  alpine:
    path: /var/lib/enodia/cve/alpine           # https://secdb.alpinelinux.org/<branch>/{main,community}.json
  mariadb:
    path: /var/lib/enodia/cve/mariadb.md       # https://mariadb.com/docs/server/security/cve/community-server.md
  atlassian:
    path: /var/lib/enodia/cve/atlassian.json   # https://api.atlassian.com/vuln-transparency/v1/products
  postgresql:
    path: /var/lib/enodia/cve/postgresql       # https://www.postgresql.org/support/security/ (+ /<major>/ pages)
  nginx:
    path: /var/lib/enodia/cve/nginx.html       # https://nginx.org/en/security_advisories.html

Updating the databases

enodia cve update                        # every cve.*.path in the active config
enodia cve update --from inventory.jsonl # also the OVAL releases, Alpine branches and
                                         # PostgreSQL majors that inventory's hosts need
enodia cve update --dry-run              # list what would be fetched

It fetches into each configured path what that entry reads: BDU's .zip, NVD's yearly files (this year, last year and any year not on disk yet; --all-years refreshes all of them — NVD rebuilds every yearly file daily), the Debian tracker's .json, MariaDB's, Atlassian's, nginx's pages and PostgreSQL's main page. For OVAL, Alpine and PostgreSQL's per-major pages it fetches the releases already in those directories, the ones the --from inventories need, and any given with --oval ubuntu:noble, --alpine v3.22 or --postgresql 13. So oval.path, alpine.path and nvd.path must be directories, bdu.path a .zip and debian.path a .json; a postgresql.path that is a file gets the main page only.

Each file is requested If-Modified-Since its copy on disk, downloaded beside it, loaded by the same code check uses, and only then moved over the old copy: an unchanged file costs one request, and a failed or broken download never replaces a working one. The exit status is 1 if any file failed; the others are still updated. Run it from cron; the next check or serve collection picks the new files up.

TLS is verified against the system's trusted roots. bdu.fstec.ru's chain ends at the Russian Trusted Root CA, which most systems don't carry, and the server doesn't send its intermediate — add both, or turn verification off:

cve:
  update:
    ca_file: /etc/enodia/russian-trusted.pem  # added to the system roots; PEM (one or many) or DER
    ca_dir: /etc/enodia/ca                    # every certificate file in it, likewise
    tls_skip_verify: false                    # true: verify nothing, for every download

The Root CA and "Russian Trusted Sub CA" (2024) are published at http://nuc-cdp.digital.gov.ru/cdp/rootca_ssl_rsa2022.crt and http://nuc-cdp.digital.gov.ru/cdp/subca_ssl_rsa2024.crt.

Sources

Each block works alone. bdu.path is the export as published (.zip), or the .xml inside it, or a .tar.gz. nvd.path is one file or a directory of NVD's yearly files (.json, .json.gz or .json.zip, from https://nvd.nist.gov/feeds/json/cve/2.0/). Relative paths resolve against the config file's own directory. The block is read from whichever enodia.yaml the run uses (see "File locations").

On Windows any of these work: C:\enodia\cve\nvd unquoted, 'C:\enodia\cve\nvd' in single quotes, C:/enodia/cve/nvd, or a UNC path. Avoid double quotes — YAML turns \t and \n inside them into a tab and a newline, so "C:\temp\nvd" is rejected at load with a hint.

The first run after a database file changes parses it in full — about a minute for all of NVD plus BDU — and caches the result in the OS cache directory (~/.cache/enodia/cve, %LocalAppData%\enodia\cve). Every later run reads the cache in under a second.

debian.path is the Debian Security Tracker's JSON export (.json, .json.gz or .json.zip). It is matched per installed package, not per release: the debian probe also reads the host's source packages (dpkg-query) and running kernel (uname -v) in the same SSH round trip. A package is flagged when Debian has already fixed a CVE in a newer version than the one installed, so each finding is something apt upgrade (and, for the kernel, a reboot) would close. CVEs Debian hasn't fixed yet are left out. The tracker only covers releases its security team still supports (bookworm, trixie, testing, sid), so older hosts get no package findings. The file parses in about a second, so it isn't cached.

oval.path is one vendor OVAL file or a directory of them, as published (.xml or .xml.bz2), one per release in your fleet. ubuntu, linuxmint, rhel, rocky-linux, almalinux, oracle-linux, astra-linux and redos targets are matched per installed package the same way:

Targets File
Ubuntu, Linux Mint (its Ubuntu base) https://security-metadata.canonical.com/oval/com.ubuntu.<codename>.usn.oval.xml.bz2 (not the oci. variant)
RHEL, and Rocky Linux https://security.access.redhat.com/data/oval/v2/RHEL<N>/rhel-<N>.oval.xml.bz2
AlmaLinux https://security.almalinux.org/oval/org.almalinux.alsa-<N>.xml.bz2
Oracle Linux https://linux.oracle.com/security/oval/com.oracle.elsa-ol<N>.xml.bz2
Astra Linux SE https://dl.astralinux.ru/astra/oval/<1.7|1.8>_x86-64/oval-definitions-alse-<1.7|1.8>.xml
RED OS https://redos.red-soft.ru/support/secure/<7.3|8.0>/redos.xml

Rocky's own OVAL file is refused: it is far from complete, so Rocky hosts are matched against Red Hat's. Parsed OVAL is cached like BDU and NVD.

alpine.path is Alpine's secdb, a file or a directory of the main.json and community.json files of each branch in your fleet (download them under distinct names, e.g. v3.20-main.json). alpine-linux targets are matched per origin package.

mariadb.path is MariaDB's own table of fixed CVEs, the Markdown page above saved as is. It lists, for every CVE, the release that fixes it in each maintained series, so a mariadb target is judged on its own series: 10.11.19 is not flagged for a CVE fixed only in 11.4 and up. A series that had already ended when a CVE was fixed elsewhere is flagged, with the newer release to move to. It merges with BDU and NVD (which also cover mariadb): for a CVE MariaDB's table knows, its verdict wins, because BDU's and NVD's per-series ranges often reach into series the bug never existed in. CVEs the table doesn't list yet still come from BDU and NVD.

atlassian.path is Atlassian's vulnerability transparency export, the JSON that URL returns, saved as is. It lists Jira, Confluence, Bitbucket and Bamboo releases (Server and Data Center) with the CVEs each is affected by and the release that fixes each, third-party dependencies included. A target is judged within its own branch: Jira 10.3.26 is not flagged for a CVE Atlassian lists only for 10.1 and 11.3. A branch with no fix listed after an affected release is flagged to its end. It merges with BDU and NVD like MariaDB's table: for a release Atlassian lists, its verdict wins on every CVE it tracks; a release newer than the file keeps BDU's and NVD's findings.

postgresql.path is the PostgreSQL project's security page saved as HTML, or a directory of such pages. The main page names, for every CVE, the supported majors it affects and the release that fixes each. A major no longer supported isn't named there; save its own page (/support/security/13/) into the same directory to cover it. A major that had ended before a CVE came out, when the CVE reaches back to the oldest major still supported then, is flagged with no fix. For a major the pages name, the project's verdict replaces BDU's, whose PostgreSQL ranges have no lower bound ("до 18.5") and flag every older major's latest release.

nginx.path is nginx's security advisories page saved as HTML. Each advisory lists the vulnerable versions and, per branch, the first fixed release ("1.31.6+, 1.30.5+"). A stable release with the fix (1.30.5) is no longer flagged by a range written up to the mainline fix ("до 1.31.0"). Branches that never got the fix stay flagged. Advisories for nginx/Windows only are skipped.

A Proxmox VE host gets package findings as a second, SSH debian target alongside its API proxmox one.

Findings show up as a CVES count in check's compact and drift views and as a per-CVE list in export --format html, with Debian, OVAL and Alpine findings grouped per package (linux 6.12.107-1 → 6.12.111-1, linked to the advisory that fixes it, with its CVE list folded). export --format json carries every finding with its source. Which products are matched, and why some deliberately aren't, is in docs/DECISIONS.md D30–D35, D42–D46 and D50.

File locations

Both files are found the same way: an explicit path always wins and must exist (a typo must be an error, never a silent fall-through to some other file), then a search — first match wins outright, nothing is merged from several found files. Location beats naming: a match in the current directory always wins over one in $XDG_CONFIG_HOME, which always wins over one in /etc/enodia/, regardless of which name matched where.

enodia.yaml (--config <path> or $ENODIA_CONFIG for an exact file):

  1. ./enodia.yaml
  2. ./enodia.yml
  3. ./config.yaml
  4. ./config.yml
  5. ./.enodia.yaml
  6. ./.enodia.yml
  7. ./.config.yaml
  8. ./.config.yml
  9. $XDG_CONFIG_HOME/enodia/enodia.yaml (~/.config/enodia/enodia.yaml if $XDG_CONFIG_HOME is unset)
  10. $XDG_CONFIG_HOME/enodia/enodia.yml
  11. $XDG_CONFIG_HOME/enodia/config.yaml
  12. $XDG_CONFIG_HOME/enodia/config.yml
  13. /etc/enodia/enodia.yaml
  14. /etc/enodia/enodia.yml
  15. /etc/enodia/config.yaml
  16. /etc/enodia/config.yml

Finding nothing at all is an error: a config that can't be found is worth failing loudly over, since it usually means the wrong file (or none) is about to be used.

settings.yaml (--settings <path> or $ENODIA_SETTINGS for an exact file) — same idea, with three differences: it also checks a plain settings. name (not just enodia.settings.), it also checks the directory containing the running executable (not just cwd — the actual "next to the binary" case, which matters most on Windows: install.ps1 puts enodia.exe in %LOCALAPPDATA%\enodia and adds that to PATH, so cwd is rarely the install directory), and finding nothing at all is not an error — every field just falls back to its built-in default, since this file is entirely optional. enodia.yaml does not get this executable-directory step: it carries credentials, so it stays out of shared/portable install directories on purpose.

  1. ./enodia.settings.yaml
  2. ./enodia.settings.yml
  3. ./settings.yaml
  4. ./settings.yml
  5. ./.enodia.settings.yaml
  6. ./.enodia.settings.yml
  7. ./.settings.yaml
  8. ./.settings.yml
  9. <directory of the running executable>/settings.yaml
  10. <same>/settings.yml
  11. $XDG_CONFIG_HOME/enodia/settings.yaml (~/.config/enodia/settings.yaml if $XDG_CONFIG_HOME is unset)
  12. $XDG_CONFIG_HOME/enodia/settings.yml
  13. /etc/enodia/settings.yaml
  14. /etc/enodia/settings.yml

Third-party assets

html.assets: cdn loads Bootstrap and, unless html.theme: none, a Bootswatch theme of it — both MIT licensed — from jsdelivr or cdnjs at the moment someone opens the report in a browser. Neither is bundled into this repository or into any release artifact; every CDN-mode report credits both by name with a link to their license in its own footer. The default html.assets: inline mode loads nothing external at all — see "Reporting" above.

Security

Enodia holds credentials to your infrastructure. Consequences, all deliberate:

  • Credentials never appear in the inventory, in exported reports, or in logs.
  • HTTPS is tried before HTTP. Credentials are never sent over plain HTTP unless you explicitly opt in per service.
  • TLS verification is on by default. Custom CA and certificate pinning are supported so that insecure: true stays a last resort — and services checked without verification are flagged in the report.
  • Secrets live in a separate credentials.yaml or environment variables, so your service inventory can be committed to git and your secrets cannot.

Found a hole? See SECURITY.md.

Contributing

Adding a product is one file plus one line in the registry, and a recorded vendor response in testdata/ so it stays honest. See CONTRIBUTING.md.

Contributions require signing the CLA — the bot handles it on your first pull request. This exists so the project can be offered under commercial terms alongside the AGPL; you keep the copyright to your work.

Licence

Enodia is licensed under AGPL-3.0-or-later. See LICENSE.

If the AGPL does not fit your situation, a commercial licence is available — contact <developer@epicm.org>.

The name

Enodia, "she of the wayside", is an epithet of Hecate: torchbearer, keeper of crossroads. Fitting for something that lights up what is decaying and stands where you choose which way to upgrade.

About

You describe your services once. Enodia handles the rest.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages