# Drupal Module Health Monitor — Releases and Security (`atesen-software/drupal-module-health-monitor`) Actor

Check Drupal modules using public project and Composer data: maintenance status, stable releases, core compatibility, installs and open security advisories. Flags stale or unsupported projects and known security issues. No login or proxy required.

- **URL**: https://apify.com/atesen-software/drupal-module-health-monitor.md
- **Developed by:** [Atesen Software](https://apify.com/atesen-software) (community)
- **Categories:** Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$10.00 / 1,000 completed project health reports

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

Check Drupal modules on drupal.org: health flag, maintenance status, latest stable release for Drupal 10 and 11, installs and open security advisories.

### Drupal Module Health Monitor

Give it a list of drupal.org project names (or a search term) and get one row per project with a simple **health flag (OK, STALE, UNSUPPORTED or SECURITY)** and the facts behind it: **maintenance status, development status, the latest stable release for each Drupal core major (10.x and 11.x) with its release date, which core versions the newest release supports, reported installs and the open security advisories (SA-CONTRIB id, date, risk)**. It reads drupal.org's public data only. No login, no browser, no proxy.

- **Built for agency audits**: paste the modules of a client site and see in one table which ones are safe, which are going stale and which have unfixed security advisories.
- **Per-core answer**: for every Drupal core major you choose, the newest stable release that works with it. A module that is ready for Drupal 10 but not yet for 11 shows exactly that.
- **Open advisories only**: an advisory counts as open when it still affects the newest stable release. Fixed advisories are counted in `advisoriesTotal` but do not raise the flag.
- **Never crashes on a bad name**: an unknown project, a typo or a failed lookup gives one free `status` row that says why.
- **No personal data**: maintainer and author names are not saved.

### Use cases

- **Client site audits**: a monthly health table for every module a client runs.
- **Upgrade planning, Drupal 10 to 11**: which modules already have a stable release for 11.
- **Security watch**: modules that carry an unfixed SA-CONTRIB advisory, including abandoned ones that drupal.org marks unsupported.
- **Module selection**: compare candidate modules by maintenance status, last release and installs before you build on them.

### Input

| Field | Description |
|---|---|
| `projects` | drupal.org machine names (`token`) or project URLs (`https://www.drupal.org/project/pathauto`). Modules, themes and recipes work. Drupal core itself is not checked. |
| `searchTerms` | Optional. Words for drupal.org's package search, for example `sitemap`. The best matches are checked. |
| `coreVersions` | Core majors you care about. Default 10 and 11. Choices: 9, 10, 11, 12. |
| `maxItems` | Total number of projects to check. Default 25, `0` = no limit. |
| `maxSearchResults` | Matches checked per search term. Default 10, up to 100 (the package search answers 50 matches per page; further pages are read as needed, 10 seconds apart). |
| `staleAfterMonths` | Age of the newest stable release after which a project is flagged STALE. Default 18. |
| `includeAdvisoryList` | Default on. Adds the open advisories to every row. |

```json
{
  "projects": ["token", "https://www.drupal.org/project/webform", "pathauto", "raw_formatter"],
  "maxItems": 10
}
```

drupal.org asks API clients for one request every 10 seconds, so plan on roughly 25 seconds per project (two lookups per project plus one shared advisory lookup for every ten projects).

### Output

One row per project.

A healthy project. Webform has 31 advisories in its history, none of them affects the newest release, so the flag is OK:

```json
{
  "type": "project",
  "name": "Webform",
  "machineName": "webform",
  "url": "https://www.drupal.org/project/webform",
  "projectType": "project_module",
  "composerPackage": "drupal/webform",
  "healthFlag": "OK",
  "healthReasons": [],
  "maintenanceStatus": "Actively maintained",
  "developmentStatus": "Under active development",
  "securityCoverage": "covered",
  "latestStableRelease": "6.3.1",
  "latestStableReleaseDate": "2026-09-23",
  "supportedCoreVersions": [
    "10",
    "11"
  ],
  "latestStableByCore": {
    "10": {
      "version": "6.3.1",
      "date": "2026-09-23",
      "coreRequirement": "^10.3 || ^11.0"
    },
    "11": {
      "version": "6.3.1",
      "date": "2026-09-23",
      "coreRequirement": "^10.3 || ^11.0"
    }
  },
  "newerPreRelease": null,
  "reportedInstalls": 357014,
  "installsByBranch": {
    "5.x-2.x": 23,
    "6.x-2.x": 405,
    "6.x-3.x": 2396,
    "7.x-4.x": 68496,
    "8.x-5.x": 13749,
    "6.0.x": 3389,
    "6.2.x": 64207,
    "8.x-4.x": 2,
    "5.x-1.x": 8,
    "7.x-3.x": 22418,
    "6.1.x": 17273,
    "6.3.x": 164452,
    "6.x": 196
  },
  "openAdvisoryCount": 0,
  "openAdvisories": [],
  "advisoriesTotal": 31,
  "lastAdvisoryDate": "2026-09-23",
  "license": "GNU General Public License, version 2 or later",
  "description": "Build forms to collect data from site visitors. Create everything from a simple contact form to a multi-page form with many fields, conditional logic, and email notifications.",
  "createdAt": "2004-04-26T20:54:31.000Z",
  "projectChangedAt": "2026-07-21T16:37:33.000Z",
  "input": "https://www.drupal.org/project/webform",
  "scrapedAt": "2026-10-24T01:05:14.315Z"
}
```

A project with an open advisory. The long description is cut at 300 characters with "…":

```json
{
  "type": "project",
  "name": "Raw Formatter [Meta Tag Formatter]",
  "machineName": "raw_formatter",
  "url": "https://www.drupal.org/project/raw_formatter",
  "projectType": "project_module",
  "composerPackage": "drupal/raw_formatter",
  "healthFlag": "SECURITY",
  "healthReasons": [
    "1 open security advisory affects the latest stable release: SA-CONTRIB-2026-077",
    "Maintenance status is \"Unsupported\"",
    "Security advisory coverage was revoked"
  ],
  "maintenanceStatus": "Unsupported",
  "developmentStatus": "No further development",
  "securityCoverage": "revoked",
  "latestStableRelease": "2.0.2",
  "latestStableReleaseDate": "2026-07-08",
  "supportedCoreVersions": [
    "10",
    "11"
  ],
  "latestStableByCore": {
    "10": {
      "version": "2.0.2",
      "date": "2026-07-08",
      "coreRequirement": "^10 || ^11"
    },
    "11": {
      "version": "2.0.2",
      "date": "2026-07-08",
      "coreRequirement": "^10 || ^11"
    }
  },
  "newerPreRelease": null,
  "reportedInstalls": 71,
  "installsByBranch": {
    "2.0.x": 63,
    "8.x-1.x": 8
  },
  "openAdvisoryCount": 1,
  "openAdvisories": [
    {
      "id": "SA-CONTRIB-2026-077",
      "date": "2026-07-08",
      "risk": "Critical",
      "type": "Unsupported",
      "title": "Raw Formatter [Meta Tag Formatter] - Critical - Unsupported - SA-CONTRIB-2026-077",
      "cve": "CVE-2026-15086",
      "affectedVersions": "*",
      "url": "https://www.drupal.org/sa-contrib-2026-nnvker3eee"
    }
  ],
  "advisoriesTotal": 1,
  "lastAdvisoryDate": "2026-07-08",
  "license": "GNU General Public License, version 2 or later",
  "description": "This module is unsupported due to a security issue the maintainer didn’t fix. If you want to use this module, your options are: Choose another, actively maintained module instead Following the unsupported project process . Hire someone to fix the security bug so the module can be re-published and s…",
  "createdAt": "2015-10-09T05:56:14.000Z",
  "projectChangedAt": "2026-07-08T17:13:30.000Z",
  "input": "raw_formatter",
  "scrapedAt": "2026-10-24T01:05:14.324Z"
}
```

Field notes:

- `healthFlag` is decided in this order: **SECURITY** (at least one open advisory affects the newest stable release for your core versions), **UNSUPPORTED** (maintenance status "Unsupported", development status "Obsolete", security coverage revoked, or no release at all that works with your core versions), **STALE** (the newest stable release for your core versions is older than `staleAfterMonths`), otherwise **OK**. `healthReasons` lists every reason, plus notes such as "Seeking new maintainer". A note also says so when something could not be checked: a status that drupal.org lists but that could not be read, a release without a date, or an advisory whose affected-version range could not be read (that advisory is then named and not counted as open).
- `latestStableRelease` is the newest stable release overall. `latestStableByCore` has one entry per core major you asked for: the newest stable release whose Composer requirement admits that core, with `coreRequirement` as the module declares it. `supportedCoreVersions` lists the core majors (8 to 12) the newest release admits.
- When no stable release exists for your cores but a pre-release (alpha, beta, rc) does, the flag is judged on that pre-release and `healthReasons` says so. `newerPreRelease` shows a pre-release that is newer than the latest stable one.
- `reportedInstalls` is the sum of the sites that report using the project, over all branches, as drupal.org counts them; `installsByBranch` shows the split. Drupal 7 and older sites are included in the sum, so use the branch split for Drupal 8 and newer.
- `openAdvisories` entries have `id` (SA-CONTRIB-YYYY-NNN), `date`, `risk` (Critical, Highly critical, Moderately critical, Less critical, Not critical), `type`, `cve`, `affectedVersions` and `url`.

Free helper rows share the same dataset: `type: "status"` with `status`, `input`, `message`, and `project` (a project problem) or `httpStatus` (a failed search).

Status values you may see: `project_not_found`, `invalid_project`, `unsupported_project`, `search_no_results`, `search_failed`, `blocked_by_robots`, `fetch_failed`, `limit_reached`, `invalid_input`.

```json
{
  "type": "status",
  "status": "project_not_found",
  "input": "no_such_project_zzz",
  "message": "No project with the machine name \"no_such_project_zzz\" exists on drupal.org. Not charged.",
  "project": "no_such_project_zzz",
  "scrapedAt": "2026-10-24T01:07:30.253Z"
}
```

### Where the data comes from

All data is public and comes from drupal.org's own services:

- **Project data**: `www.drupal.org/api-d7/node` (maintenance status, development status, security coverage, reported installs, license, summary).
- **Releases**: the drupal.org Composer repository at `packages.drupal.org` (release numbers, dates, and the Drupal core each release requires).
- **Security advisories**: the security advisory feed of the same Composer repository (SA-CONTRIB ids, risk, dates, affected versions).
- **Search**: the package search of that repository.

The update status XML at `updates.drupal.org/release-history/<project>/current` is deliberately not used. That host's `robots.txt` disallows all robots, so the Actor stays away from it and gets the release data from the Composer repository instead, which also states the core requirement of every release.

#### Drupal.org API terms (checked 24 October 2026)

- The API guide asks clients to use the APIs respectfully: an appropriate user agent, requests from a single thread, local caching, and it says abuse will be blocked. This Actor identifies itself as `DrupalModuleHealth/1.0`, sends one request at a time, and asks for each project once per run.
- `robots.txt` on drupal.org sets `Crawl-delay: 10`. The Actor waits at least 10 seconds between requests to any drupal.org host and checks `robots.txt` before every URL.
- The Drupal Association's page on automation tools says traditional API requests are allowed by default but rate limited, and that read-only tools may be blocked or rate limited. It also reserves the right to prohibit using content scraped from drupal.org to train commercial AI models without written permission. This Actor only reads; do not use its output for that purpose.
- You are responsible for how you use the data. Check the current terms before you build a service on it.

### Limits

- Needs a Composer-packaged release (Drupal 8 and newer). A Drupal 7 only project is reported as UNSUPPORTED with a reason.
- An advisory is judged against the newest stable release per core you chose, not against the version installed on a site. Installed versions are not an input.
- The health flag is a screening aid. It does not read code and does not replace the security team's advisories or your own review.
- Core compatibility comes from the release's Composer requirement (`drupal/core`). A module with no declared core requirement counts as not compatible.
- The search returns the repository's best matches, which can include themes and recipes.

### FAQ

**Why does it take so long?** drupal.org's `robots.txt` asks for 10 seconds between requests and its API guide asks for a single thread. A project needs two lookups, so a run of 25 projects takes about ten minutes. The Actor never goes faster than that.

**What does SECURITY mean exactly?** At least one security advisory (SA-CONTRIB) lists the newest stable release as affected. Typical cases are modules that drupal.org marked unsupported after an unfixed vulnerability. An advisory that was fixed in the latest release does not count.

**Why is a module flagged UNSUPPORTED although it exists?** Either its maintenance status is "Unsupported" or "Obsolete", its security coverage was revoked, or none of its releases works with the core versions you selected. `healthReasons` names the cause.

**Can I check my own site's installed versions?** Not yet. The Actor checks the newest releases against drupal.org's advisories. Feed it the module names from your `composer.lock` and compare the versions yourself.

**Does it use the Drupal.org update status XML?** No. That endpoint's `robots.txt` disallows robots, so the same facts are read from the Composer repository.

### Pricing

Pay per result — see the Pricing tab.

Each checked project row is one result. Status rows are free. The run stops when it reaches the maximum cost per run that you set. If no result fits the remaining budget, it makes no robots, search or project-data requests. On graceful abort it saves `RUN_SUMMARY` and a `STATE` progress snapshot (completed project names, current project and counters) before shutdown; the snapshot does not automatically resume a later run.

### Support and disclaimers

Questions and feature requests belong on the **Issues** tab of this Actor's page on Apify. This Actor is an independent tool and is not affiliated with the Drupal Association or any module maintainer. Drupal is a registered trademark of Dries Buytaert.

# Actor input Schema

## `projects` (type: `array`):

drupal.org project machine names ("token", "webform") or project URLs ("https://www.drupal.org/project/pathauto"). Modules, themes and recipes work; Drupal core itself is not checked.

## `searchTerms` (type: `array`):

Optional. Words to look for in drupal.org's package search (for example "sitemap"). The best matches are checked, up to "Matches per search term".

## `coreVersions` (type: `array`):

Which Drupal core majors you care about. For each one the Actor reports the newest stable release that works with it; the health flag is judged against them.

## `maxItems` (type: `integer`):

Stop after checking this many projects in total. 0 = no limit. drupal.org asks API clients for one request every 10 seconds, so a project takes about 25 seconds.

## `maxSearchResults` (type: `integer`):

How many of the best package-search matches to check for each search term.

## `staleAfterMonths` (type: `integer`):

A project is flagged STALE when its newest stable release for your core versions is older than this.

## `includeAdvisoryList` (type: `boolean`):

Adds the open security advisories (id, date, risk, type, CVE, affected versions, link) to each project row.

## Actor input object example

```json
{
  "projects": [
    "token",
    "webform",
    "pathauto"
  ],
  "coreVersions": [
    "10",
    "11"
  ],
  "maxItems": 25,
  "maxSearchResults": 10,
  "staleAfterMonths": 18,
  "includeAdvisoryList": true
}
```

# Actor output Schema

## `projects` (type: `string`):

No description

## `allFields` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "projects": [
        "token",
        "webform",
        "pathauto"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("atesen-software/drupal-module-health-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "projects": [
        "token",
        "webform",
        "pathauto",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("atesen-software/drupal-module-health-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "projects": [
    "token",
    "webform",
    "pathauto"
  ]
}' |
apify call atesen-software/drupal-module-health-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,atesen-software/drupal-module-health-monitor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/3UnoycqBt8aAwLv8Y/builds/JC205SemhXkZVNixW/openapi.json
