Custom Plugins

Plugin Development Guide

This document is your definitive guide to writing a plugin that works with the Sentinel system. Follow it carefully to ensure your plugin passes verification and integrates smoothly.

1. Overview

A plugin in our system is a Python module that transforms uploaded CSV data into a Polars LazyFrame (pl.LazyFrame) with a specific schema. Your plugin will be run through a verification step to ensure:

1. It can process uploaded files without error.

2. It produces the expected columns and data types.

3. It follows specific rules for VA (Vulnerability Assessment) or Compliance workflows.

2. Plugin Lifecycle

Here’s what happens under the hood when your plugin runs:

  1. File ingestion

    • The system reads one or more uploaded CSV files.

  2. Plugin execution

    • Your plugin’s process(data: bytes) function is called.

    • The function must return either:

      • A Polars LazyFrame (pl.LazyFrame)

      • A Polars DataFrame (pl.DataFrame) – will be converted to LazyFrame

      • A Pandas DataFrame – will be converted to Polars LazyFrame

  3. Schema verification

    • The system validates your output against the required schema.

    • If column names or data types do not match, the system raises an InvalidInput exception.

3. Required Schema

Your plugin must produce a DataFrame or LazyFrame with these exact columns and types.

Order does not matter, but names and types must match. ❌ Extra or missing columns are not allowed.

VA Schema

Column
Type
Description

cve

pl.String()

CVE identifier (e.g., CVE-2023-12345)

risk

pl.String()

Risk level (see acceptable values below)

host

pl.String()

Host/IP where the issue was found

port

pl.Int64()

Port number

name

pl.String()

Finding name

description

pl.String()

Human-readable explanation of the finding

remediation

pl.String()

How to fix the issue

evidence

pl.String()

Proof/details of the finding

vpr_score

pl.String()

Vendor or risk score

Compliance Schema

Column
Type
Description

risk

pl.String()

Risk level (see acceptable values below)

host

pl.String()

Host/IP where the issue was found

port

pl.Int64()

Port number

name

pl.String()

Finding name

description

pl.String()

Human-readable explanation of the finding

remediation

pl.String()

How to fix the issue

evidence

pl.String()

Proof/details of the finding

status

pl.String()

Finding status (see acceptable values below)

4. Acceptable Values

Risk

  • CRITICAL

  • HIGH

  • MEDIUM

  • LOW

  • None (Only for Compliance)

None must not be a string. Make sure that it's not be stringify.

For compliance: None will be defaulted to MEDIUM

For VA: None is not valid value

*Risk will be referred as severity internally

Status

VA

  • NEW

  • OPEN

  • CLOSED

  • EXEMPTION

  • OTHERS

Even EXEMPTION and OTHERS are valid values but these typically set manually rather than uploaded.

Compliance

  • PASSED

  • FAILED

  • WARNING

5. VA vs Compliance Plugins

Your plugin’s output is treated differently depending on whether it’s used for VA or Compliance (HA).

VA Plugins

  • Everything must be provided by the plugin.

  • All fields (risk, description, etc.) must already be populated.

  • The system does not fill defaults for you.

Compliance Plugins

For compliance plugins, the system will apply post-processing before saving findings:

# Example of what happens automatically
self.finding_lf = self.finding_lf.with_columns(
    # Default severity
    severity=pl.col("risk").fill_null(SeverityEnum.MEDIUM.value),
    # Ensure evidence not null
    evidence=pl.col("evidence").fill_null(""),                 
)
  • You should output risk as null if unknown, not as ""

6. Example Plugin Template

Note that the file need to accept bytes not the filename.

Using polars

import polars as pl

def process(data: bytes) -> pl.LazyFrame:
    df = pl.read_csv(file)
    df = df.with_columns(
        risk=pl.when(pl.col("risk") == "")
                .then(None)  # Ensure null for HA workflow
                .otherwise(pl.col("risk"))
    )

    # Optional can just return the dataframe
    return df.lazy() 

Using Pandas

Note that in pandas you need to convert bytes into BytesIO

import pandas as pd
import io

def process(data: bytes) -> pd.DataFrame:
    data_csv = io.BytesIO(data)
    df = pd.read_csv(data_csv)
    # Do necessary transformation

    return df

7. Checklist

✅ Before submitting your plugin, verify:

Last updated