# Writing Custom Checks

NoiseGate's check system is built on [pluggy](https://pluggy.readthedocs.io/). You can add your own checks by writing a plugin and registering it via a Python entry point.

---

## How checks work

When `noisegate triage` runs, it:

1. Loads your policy file (`~/.config/noisegate/policy.yaml`) to get the list of checks to run.
2. For each check, calls the registered plugin that handles that check ID.
3. Collects the results, derives a verdict (`PASS` / `WARN` / `FAIL`), and displays output.

---

## Example: a simple custom check

Subclass `CheckPlugin` and decorate each handler method with `@handles`:

```python
# my_noisegate_plugin.py

from noisegate.checks import CheckPlugin, handles
from noisegate.models import Check, CheckResult, Program, Report


class MyChecks(CheckPlugin):
    @handles("my_org.has_cve_reference")
    def _check_cve(self, check: Check, report: Report, program: Program) -> CheckResult:
        passed = "CVE-" in report.raw_text
        return CheckResult(
            check_id=check.id,
            passed=passed,
            reason="CVE reference found." if passed else "No CVE reference in report.",
        )


plugin = MyChecks()
```

Return `None` from any method that doesn't handle the given check ID — or just don't register a handler for it. NoiseGate will try the next plugin.

In triage output, check IDs are automatically formatted as titles (e.g. `my_org.has_cve_reference` → "My Org.Has Cve Reference"). Built-in checks have custom display labels.

---

## Registering the plugin

In your package's `pyproject.toml`, register the **instance** (not the class) under the `noisegate` entry point group:

```toml
[project.entry-points.noisegate]
my_plugin = "my_package.plugin:plugin"
```

After installing your package (`pip install -e .`), NoiseGate will automatically discover and load it.

---

## Adding the check to your policy

Add the check ID to `~/.config/noisegate/policy.yaml`:

```yaml
checks:
  - id: asset_in_scope
    hard_fail: true
  - id: finding_ineligible
    hard_fail: true
  - id: required_sections_present
  - id: reproduction_quality
  - id: impact_quality
  - id: my_org.has_cve_reference   # your custom check
```

Use namespaced IDs (e.g. `my_org.check_name`) to avoid collisions with built-in check IDs.

---

## Passing configuration to a check

The `Check` model has a `config` field — a free-form dict populated from your policy YAML. Use it to parameterize checks without hardcoding values:

```yaml
# policy.yaml
checks:
  - id: my_org.keyword_check
    config:
      required_keywords: ["CVSS", "CWE"]
      case_sensitive: false
```

```python
@handles("my_org.keyword_check")
def _check_keywords(self, check: Check, report: Report, program: Program) -> CheckResult:
    keywords = check.config.get("required_keywords", [])
    case_sensitive = check.config.get("case_sensitive", True)
    text = report.raw_text if case_sensitive else report.raw_text.lower()

    missing = [k for k in keywords if (k if case_sensitive else k.lower()) not in text]
    passed = not missing
    reason = "All required keywords present." if passed else f"Missing keywords: {', '.join(missing)}."
    return CheckResult(check_id=check.id, passed=passed, reason=reason)
```

---

## Using `hard_fail`

Any check can be set as `hard_fail: true` in the policy. A failed `hard_fail` check produces a `FAIL` verdict immediately, regardless of other check results. This is controlled by the policy, not the plugin — your plugin only needs to return `passed: true/false`.

---

## Available context

Your handler receives:

| Parameter | Type | Contents |
|-----------|------|----------|
| `check` | `Check` | The check being evaluated: `id`, `hard_fail`, `config` |
| `report` | `Report` | The bug report: `raw_text`, `source_path`, `char_count` |
| `program` | `Program` | The bug bounty program: `name`, `platform`, `assets`, `ineligible_findings`, `required_report_sections`, `notes` |

All types are importable from `noisegate.models`.
