Rulezet / Developer Documentation
v1.5.0

Rulezet

Community-driven cybersecurity detection rules platform. Centralise, share, validate and manage YARA, Sigma, Suricata, Zeek, CRS, Wazuh, Elastic, NSE and Nova rules at scale.

Flask 3 Flask-RESTX SQLAlchemy PostgreSQL Vue.js 3 Bootstrap 5.3 Flask-Login Flask-Migrate FAISS TF-IDF RapidFuzz GitPython

Introduction

Rulezet is a full-stack web application built with Flask (Python) on the backend and Vue.js 3 on the frontend. It lets security analysts import detection rules from GitHub repositories or create them manually, validate them, tag them, bundle them, comment on them, vote on them, and propose edits — all with a gamification layer that rewards contribution.

Supported Rule Formats
YARA Sigma Suricata Zeek CRS Nova NSE Wazuh Elastic
Entry Points
  • app.py — CLI runner, DB management
  • launch.sh — dev/test/init helper
  • app/__init__.pycreate_app() factory
  • config.py — environment configs

Architecture

Application Factory

The app uses the Flask application factory pattern. create_app() in app/__init__.py wires up every extension and blueprint, then immediately calls start_worker(app) to boot the background job thread.

# app/__init__.py
def create_app(config_name=None) → Flask:
  # Initialises: db, csrf, migrate, login_manager, sess, mail
  # Registers: all blueprints, api_blueprint (CSRF-exempt)
  # Calls: start_worker(app)

Extension Initialization Order

ExtensionObjectPurpose
Flask-SQLAlchemydbORM, session management
Flask-MigratemigrateAlembic-based schema migrations
Flask-Loginlogin_managerSession auth, current_user
Flask-WTF / CSRFProtectcsrfCSRF tokens (exempted for API)
Flask-SessionsessServer-side sessions (SQLAlchemy or filesystem)
Flask-MailmailEmail notifications
Flask-RESTXapiREST API + Swagger UI at /api/

Directory Layout

rulezet-core/
├── app/
│   ├── __init__.py           # create_app() factory
│   ├── core/
│   │   ├── db_class/db.py    # ALL SQLAlchemy models (1600+ lines)
│   │   └── utils/            # decorators.py, utils.py
│   ├── api/                  # Flask-RESTX namespaces
│   │   ├── api.py            # Api object, namespace registration
│   │   ├── rule/             # rule_public_api.py, rule_private_api.py
│   │   ├── bundle/           # bundle_public_api.py, bundle_private_api.py
│   │   └── account/          # account_public_api.py, account_private_api.py
│   ├── features/
│   │   ├── rule/
│   │   │   ├── rule.py           # Flask Blueprint views
│   │   │   ├── rule_core.py      # Business logic (add/update/delete rules)
│   │   │   ├── rule_format/
│   │   │   │   ├── abstract_rule_type/rule_type_abstract.py  # RuleType ABC
│   │   │   │   ├── available_format/  # yara_format.py, sigma_format.py, …
│   │   │   │   ├── main_format.py     # format orchestration helpers
│   │   │   │   └── utils_format/
│   │   │   ├── rule_from_github/
│   │   │   │   ├── import_rule/session_class.py   # Session_class
│   │   │   │   └── update_rule/update_class.py    # Update_class
│   │   │   └── utils/similar_rules/similarity_class.py
│   │   ├── jobs/
│   │   │   ├── job_worker.py      # register_handler, start_worker
│   │   │   ├── jobs_core.py       # CRUD + lifecycle helpers
│   │   │   └── job_handlers.py    # concrete @register_handler functions
│   │   ├── bundle/
│   │   ├── tags/
│   │   ├── account/
│   │   └── home/
│   ├── modules/               # pluggable front-end modules (rulezet-cast…)
│   ├── static/css/core.css    # Design system & CSS variables
│   └── templates/             # Jinja2 + Vue.js templates
├── config.py                  # DevelopmentConfig / TestingConfig / ProductionConfig
├── app.py                     # CLI entry point
├── launch.sh                  # Helper script
└── tests/                     # pytest suite

Request Lifecycle

Browser / API Client
      │
      ▼
Flask Router
      │
      ├─► Web Blueprint (login_required via @login_required)
      │         Jinja2 Template ◄── Vue.js 3 ([[ ]] delimiters)
      │
      └─► /api/... Blueprint (CSRF-exempt)
                ├─ Public Namespaces  → no auth needed
                └─ Private Namespaces → X-API-KEY header → verif_api_key() → @api_required

Configuration

Configuration class is selected via the FLASKENV environment variable. All three classes inherit from a common Config base.

FLASKENV valueClassDatabaseSessionNotes
development (default)DevelopmentConfigpostgresql:///rulezetSQLAlchemyDEBUG=True
testingTestingConfigsqlite:///rulezet-test.sqlitefilesystemCSRF disabled, TESTING=True
productionProductionConfigpostgresql:///rulezetSQLAlchemyDEBUG=False

Key Config Keys

  • SQLALCHEMY_DATABASE_URIstrDatabase connection string
  • SECRET_KEYstrFlask session signing key
  • SESSION_TYPEstr"sqlalchemy" (dev/prod) or "filesystem" (test)
  • WTF_CSRF_ENABLEDboolFalse in testing only
  • MAIL_SERVER / MAIL_PORTstr/intEmail server configuration
  • MAX_CONTENT_LENGTHintMax upload size

Commands & Scripts

launch.sh

bash
./launch.sh -l          # Run dev server (FLASKENV=development)
./launch.sh -t          # Run test suite  (FLASKENV=testing pytest tests)
./launch.sh -i          # Init database
./launch.sh -r          # Recreate (drop + init) database
./launch.sh -d          # Delete database

app.py CLI flags

bash
python3 app.py           # Start the server
python3 app.py -i        # Create all tables (db.create_all)
python3 app.py -r        # Drop all + recreate
python3 app.py -d        # Drop all tables

Flask-Migrate

bash
flask db init            # Initialise migrations folder (once)
flask db migrate -m "msg" # Generate migration from model changes
flask db upgrade          # Apply pending migrations

Run a single test

bash
FLASKENV=testing pytest tests/test_rule_api.py::test_search_rules -v

Data Models

All models live in a single file: app/core/db_class/db.py (~1600 lines). All use db.Model from Flask-SQLAlchemy.

A before_flush SQLAlchemy event listener (receive_before_flush) auto-updates Gamification.score and level whenever a relevant model is inserted or deleted.

User & Auth

User
  • idInteger PKAuto-increment primary key
  • usernameString(50)Unique display name
  • emailString(120)Unique, used for login
  • passwordStringHashed (Werkzeug)
  • api_keyString(60)REST API key — generate_api_key()
  • is_adminBooleanAdmin flag
  • verifiedBooleanEmail verification
  • created_atDateTimeRegistration timestamp

Rule & Format

Rule
  • idInteger PK
  • uuidString(36)Stable public identifier
  • nameString
  • contentTextRaw rule text
  • format_idFK → FormatRule
  • author_idFK → User
  • github_pathStringSource file path in repo
  • github_urlStringSource repository URL
  • descriptionText
  • severityString
  • scoreIntegerVote aggregate
  • created_atDateTime
  • updated_atDateTime
FormatRule
  • idInteger PK
  • nameStringFormat identifier e.g. "yara"
  • descriptionText

InvalidRuleModel
  • id / uuidInteger / String
  • contentTextRaw invalid rule text
  • rule_typeString
  • errorTextValidation error message(s)
  • user_idFK → User

Social & Moderation

ModelKey FieldsPurpose
RuleVoteuser_id, rule_id, value (+1/-1)Upvote/downvote a rule
RuleFavoriteUseruser_id, rule_idBookmark a rule
Commentuser_id, rule_id, content, parent_idThreaded comments on rules
RepportRuleuser_id, rule_id, reasonFlag a rule for review
RequestOwnerRuleuser_id, rule_id, statusClaim authorship of a rule
RuleEditProposalrule_id, proposed_content, statusCommunity edit suggestion
RuleEditCommentproposal_id, user_id, contentComments on edit proposals
RuleEditContributionproposal_id, user_idTrack contributors of an edit
RuleUpdateHistoryrule_id, old_content, new_content, updated_atDiff history for a rule
NewRuleuser_id, format, contentPending rule submission queue
RuleStatusrule_id, status, reasonReview/approval status

Tags

Tag
  • idInteger PK
  • nameStringUnique tag label
  • colorStringHex colour for badge
  • created_byFK → User
RuleTagAssociation
  • rule_idFK → Rule
  • tag_idFK → Tag

Bundles

ModelKey FieldsPurpose
Bundleid, uuid, name, description, user_id, is_public, scoreNamed collection of rules
BundleNodebundle_id, rule_id, positionOrdered rule within a bundle
BundleRuleAssociationbundle_id, rule_idMany-to-many bridge
BundleTagAssociationbundle_id, tag_idTags on a bundle
BundleVoteuser_id, bundle_id, valueVote on a bundle
CommentBundleuser_id, bundle_id, contentComments on a bundle
BundleReactionCommentcomment_id, user_id, reactionEmoji reactions on bundle comments

GitHub & Import

ModelKey FieldsPurpose
ImporterResultuuid, info (JSON), bad_rules, imported, skipped, total, count_per_format (JSON), query_date, user_idPersisted result of a GitHub import session
UpdateResultuuid, info (JSON), updated, skipped, errors, query_date, user_idPersisted result of a GitHub update session

Background Jobs

ModelKey FieldsPurpose
BackgroundJobuuid, job_type, status (pending/running/done/failed/cancelled/paused), payload (JSON), label, created_by (FK→User), created_at, started_at, finished_atPersistent job record
BackgroundJobLogjob_id (FK→BackgroundJob), message, level, created_atLog entries for a job

Similarity

ModelKey FieldsPurpose
RuleSimilarityrule_id_a, rule_id_b, score (float), methodStores pre-computed similarity pairs
SimilarResultsession_uuid, rule_id, similar_rule_id, scorePer-session similarity results

Gamification

Gamification
  • user_idFK → UserOne-to-one with User
  • scoreIntegerCumulative points, auto-updated
  • levelIntegerDerived from score thresholds
  • rules_ownedInteger
  • suggestions_acceptedInteger
  • rules_liked_or_dislikedInteger

Gamification

Points are automatically calculated by a before_flush SQLAlchemy event. The POINTS dict and LEVEL_THRESHOLDS dict live in app/core/db_class/db.py.

Point Values

suggestions_accepted+100
rules_owned+10
rules_liked_or_disliked+1

Level Thresholds (LEVEL_THRESHOLDS)

LevelMin Score
10
2500
315 000

Event Listener

# Registered in db.py
event.listen(db.session, 'before_flush', receive_before_flush)

# receive_before_flush scans session.new / session.deleted
# for Rule, RuleVote, RuleEditContribution instances
# and adjusts the related Gamification row accordingly

Rule Format System

All format support is built around an abstract base class RuleType in app/features/rule/rule_format/abstract_rule_type/rule_type_abstract.py. Each format is a subclass discovered automatically via pkgutil.iter_modules.

ValidationResult Dataclass

  • okboolTrue if the rule passed validation
  • errorsList[str]Validation error messages
  • warningsList[str]Non-fatal warnings
  • normalized_contentOptional[str]Cleaned/normalized rule text after validation

RuleType Abstract Methods

Method / PropertySignaturePurpose
format@property → strFormat identifier (e.g. "yara")
get_class()→ strHuman-readable class name
validate(content)str → ValidationResultParse + validate raw rule text
parse_metadata(content, info, validation_result)str, dict, ValidationResult → dictExtract structured metadata dict from rule
get_rule_files(file)str → boolWhether a filename belongs to this format
extract_rules_from_file(filepath)str → List[str]Split a file into individual rule strings
find_rule_in_repo(repo_dir, rule_id)str, str → Optional[str]Locate a specific rule by ID in a cloned repo

Auto-Discovery

# rule_type_abstract.py
def load_all_rule_formats() → None:
  # Uses pkgutil.iter_modules on available_format/ package
  # Imports each module so RuleType.__subclasses__() is populated
To add a new format, create a file in app/features/rule/rule_format/available_format/ that subclasses RuleType and implements all abstract methods. It will be discovered automatically.

Supported Formats

YARA — yara_format.py

Full YaraRule(RuleType) implementation with advanced retry logic.

  • Validation: Calls yara.compile(). On undefined identifier errors, automatically prepends import "module" and retries up to 10 times.
  • Extraction: Custom brace-tracking parser that handles strings sections, regex literals, line comments, block comments.
  • File extensions: .yar, .yara

Other Formats

FormatClassLibraryExtensions
SigmaSigmaRulepySigma.yml, .yaml
SuricataSuricataRulesuricataparser, idstools.rules
ZeekZeekRulezeekscript.zeek
CRSCRSRulemsc_pyparser.conf
NovaNovaRulenova-hunting.nova
NSENSERuleLua parser.nse
WazuhWazuhRuleXML/YAML.xml, .yml
ElasticElasticRuleJSON/TOML.toml, .json

Rule Core Logic

Business logic lives in two files: app/features/rule/rule_core.py (primary CRUD) and app/features/rule/rule_format/main_format.py (format orchestration).

rule_core.py — Key Functions

def add_rule_core(metadata: dict, user: User) → tuple[bool, str]
# Inserts Rule into DB; checks duplicates, resolves FormatRule, assigns tags
def update_rule_core(rule_id: int, data: dict, user: User) → tuple[bool, str]
# Updates rule, creates RuleUpdateHistory entry
def delete_rule_core(rule_id: int, user: User) → tuple[bool, str]
def vote_rule(rule_id: int, value: int, user: User) → tuple[bool, str]
# value must be +1 or -1; creates/updates/removes RuleVote

main_format.py — Format Orchestration

def Process_rules_by_format(
  format_files, format_rule, info, format_name, user
) → tuple[int, int, int] # (bad_rules, imported, skipped)
def extract_rule_from_repo(repo_dir: str, info: dict, user: User) → None
# Async; walks repo_dir, dispatches to each RuleType subclass
def verify_syntax_rule_by_format(rule_dict: dict) → tuple[bool, str]
def parse_rule_by_format(
  rule_content: str, user: User, format_name: str,
  url_repo: str, github_path: str
) → tuple[bool, str, Rule]
def process_and_import_fixed_rule(bad_rule_obj, raw_content: str) → tuple[bool, str, Rule]
# Re-validates a previously invalid rule and imports it on success

Similarity Engine

Implemented in app/features/rule/utils/similar_rules/similarity_class.py. Combines TF-IDF vectorisation, FAISS ANN search, and RapidFuzz fuzzy scoring.

Similarity_class

Similarity_class(
  user: User,
  info: dict,
  mode: str = "global", # "global" | "targeted"
  target_rule_id: Optional[int] = None,
  params: Optional[dict] = None
)
StepToolDetail
1. Vectorisesklearn TfidfVectorizerConverts rule content to TF-IDF matrix
2. ANN searchfaiss-cpu IndexFlatIPFast approximate nearest-neighbour lookup
3. Fuzzy scorerapidfuzz.fuzz.ratioCharacter-level ratio, returns 0.0–1.0
4. PersistSQLAlchemyResults stored in RuleSimilarity
def _parallel_fuzzy_worker(batch_data: list) → list
# Runs in thread pool; applies fuzz.ratio to each rule pair in batch

GitHub Import & Update

Import Pipeline — Session_class

Defined in app/features/rule/rule_from_github/import_rule/session_class.py.

Session_class(repo_dir: str, user: User, info: dict)
  # uuid, thread_count=4, jobs Queue, repo_dir, counts (bad/imported/skipped)
MethodDescription
start()Walks repo_dir with os.walk, skips hidden dirs/files, matches files to RuleType subclasses, fills the jobs Queue, spawns 4 daemon Threads running process()
process(loc_app, user)Worker loop. Dequeues (index, file, filepath, rule_instance). Calls rule_instance.extract_rules_from_file(), validate(), parse_metadata(), then RuleModel.add_rule_core() or BadRuleModel.save_invalid_rule().
status()Returns JSON: {id, total, complete, remaining, stopped, bad_rules, imported, skipped}
stop()Clears queue, joins threads (3.5 s timeout each), calls save_info(), removes session from global sessions list, deletes cloned repo folder
save_info()Persists ImporterResult to DB with per-format counts
The info dict is enriched per-file: enriched_info = {**self.info, "github_path": filepath} before calling parse_metadata.

Update Pipeline — Update_class

Defined in app/features/rule/rule_from_github/update_rule/update_class.py.

Update_class(
  repo_sources: list,
  user: User,
  info: dict,
  mode: str # "by_url" | "by_rule"
)
ModeBehaviour
by_urlRe-clones the source repo and re-processes all rules
by_ruleFinds each individual rule in the repo using find_rule_in_repo() and updates only the changed ones
Update_class uses thread_count=1 with a threading.Lock to prevent concurrent DB writes during updates.

Background Job System

The job system provides persistent, pause-able, cancellable background tasks via a single worker thread started at app startup.

Job Lifecycle

pending → running → done
               ↓         ↑
           paused  → resumed
               ↓
           cancelled
               ↓
           failed  (exception in handler)

Worker — job_worker.py

def register_handler(job_type: str):
  # Decorator — registers a callable into _HANDLERS dict
  # Usage: @register_handler('bulk_add_tag_to_rules')
def start_worker(app: Flask) → None:
  # Called by create_app(). Starts daemon thread → _worker_loop(app)
def _worker_loop(app: Flask) → None:
  # On start: recovers any job stuck in "running" state (set to "pending")
  # Main loop: polls DB every 2 s for pending jobs
  # Dispatches to _HANDLERS[job.job_type](job, app)

jobs_core.py — Lifecycle Functions

FunctionSignature
create_job(job_type, payload, label, created_by) → BackgroundJob
get_job_by_uuid(uuid) → BackgroundJob | None
get_jobs_for_user(user) → List[BackgroundJob]
cancel_job(uuid, user) → (bool, str)
pause_job(uuid, user) → (bool, str)
resume_job(uuid, user) → (bool, str)
delete_job(uuid, user) → (bool, str)
get_zombie_jobs() → List[BackgroundJob] — running >2 h or pending >6 h
kill_all_zombies() → int — count killed

Registered Handlers — job_handlers.py

Job TypeHandler FunctionDescription
bulk_add_tag_to_ruleshandle_bulk_add_tag_to_rulesAdds a tag to all rules matching a filter set
bulk_remove_tag_from_ruleshandle_bulk_remove_tag_from_rulesRemoves a tag from filtered rules
delete_github_ruleshandle_delete_github_rulesDeletes all rules originating from a given GitHub source
BATCH_SIZE = 2000, LOG_EVERY = 10. The helper _build_rule_query(payload) mirrors the exact filter params from the UI search.

Tags & Bundles

Tags

Tags are coloured labels attached to rules via RuleTagAssociation. Bulk tag operations run as background jobs (bulk_add_tag_to_rules / bulk_remove_tag_from_rules) to handle large rule sets without blocking the request.

Bundles

Bundles are named, ordered, optionally-public collections of rules. Users can vote on bundles, comment on them, and attach tags. The BundleNode table preserves rule ordering within a bundle.

OperationModel / Function
Create bundleBundle insert
Add rule to bundleBundleNode + BundleRuleAssociation insert
Vote on bundleBundleVote upsert, updates Bundle.score
Comment on bundleCommentBundle insert
React to commentBundleReactionComment upsert

API Overview

REST API powered by Flask-RESTX. Swagger UI is served at /api/. All API routes are CSRF-exempt. The base URL for a local dev server is http://127.0.0.1:7009.

Namespace labelURL prefixAuthSource file
Public action on Rule ✅/api/rule/publicNoneapp/api/rule/rule_public_api.py
Private action on Rule 🔑/api/rule/privateX-API-KEYapp/api/rule/rule_private_api.py
Public action on Bundle ✅/api/bundle/publicNoneapp/api/bundle/bundle_public_api.py
Private action on Bundle 🔑/api/bundle/privateX-API-KEYapp/api/bundle/bundle_private_api.py
Public account action ✅/api/account/publicNoneapp/api/account/account_public_api.py
Private account action 🔑/api/account/privateX-API-KEYapp/api/account/account_private_api.py
Input can be sent as a JSON body (Content-Type: application/json) or as query string parameters for most endpoints. Private endpoints always require the X-API-KEY header.

API Authentication

Private endpoints require a 60-character alphanumeric API key passed in the X-API-KEY HTTP request header. Your key is shown in your Rulezet profile page.

Auth utilities — app/core/utils/utils.py

def generate_api_key(length: int = 60) → str
# Cryptographically-random alphanumeric key (secrets.token_hex based)
def verif_api_key(headers: dict) → bool
# Returns True if X-API-KEY header is present and matches a user row
def get_user_from_api(headers: dict) → User | None
# Resolves X-API-KEY → User object (None if key invalid)

@api_required decorator — app/core/utils/decorators.py

Wraps any private resource method. Returns 401 JSON when the key is missing or invalid.

usage
from app.core.utils.decorators import api_required

class MyPrivateResource(Resource):
    @api_required
    def get(self):
        user = utils.get_user_from_api(request.headers)
        ...

Error responses

StatusConditionBody
401Missing or invalid X-API-KEY{"message": "Unauthorized"}
403Key valid but not enough permissions{"success": false, "message": "Access denied"}

Quick test

curl
curl -X GET http://127.0.0.1:7009/api/rule/private/me \
  -H "X-API-KEY: YOUR_60_CHAR_KEY"

# 200 → {"message": "Welcome Alice!", "user_id": 4}
# 401 → {"message": "Unauthorized"}

Rule Public API

File: app/api/rule/rule_public_api.py — No authentication required. All endpoints accept GET query parameters.

GET /api/rule/public/searchPage

GET
/api/rule/public/searchPage
Paginated rule search. Returns pagination links (next/prev URLs).
ParameterTypeDefaultAllowed values / notes
searchstringKeyword to match against rule title
authorstringFilter by author username
rule_typestringyara, sigma, suricata, zeek, crs, nova, nse, wazuh, elastic
sort_bystringnewest · oldest · most_likes · least_likes
pageinteger11-based page number
per_pageinteger10Items per page
curl
curl -G "http://127.0.0.1:7009/api/rule/public/searchPage" \
  --data-urlencode "search=detect" \
  --data-urlencode "author=John" \
  --data-urlencode "sort_by=newest" \
  --data-urlencode "page=1" \
  --data-urlencode "per_page=10"
response 200
{
  "total_rules_found": 42,
  "total_pages": 5,
  "pagination": {
    "prev_page": null,
    "current_page": 1,
    "next_page": "http://127.0.0.1:7009/api/rule/public/searchPage?page=2&per_page=10&search=detect"
  },
  "results": [
    {
      "uuid": "550e8400-e29b-41d4-a716-446655440000",
      "title": "Detect_Ransomware_Generic",
      "description": "Detects generic ransomware patterns",
      "author": "analyst01",
      "creation_date": "2025-03-15T10:23:00",
      "format": "yara",
      "content": "rule Detect_Ransomware_Generic { ... }"
    }
  ]
}

GET /api/rule/public/search

GET
/api/rule/public/search
Same filters as searchPage but returns all matching rules in a single response (no pagination).
ParameterTypeAllowed values
searchstringFree text
authorstringAuthor username
rule_typestringyara · sigma · suricata · zeek · crs · nova · nse · wazuh · elastic
sort_bystringnewest · oldest · most_likes · least_likes
curl
curl -G "http://127.0.0.1:7009/api/rule/public/search" \
  --data-urlencode "search=detect" \
  --data-urlencode "author=malgamy12" \
  --data-urlencode "rule_type=yara" \
  --data-urlencode "sort_by=newest"
response 200
{
  "total_rules_found": 3,
  "results": [
    { "uuid": "...", "title": "...", "description": "...", "author": "...",
      "creation_date": "2025-01-10T08:00:00", "format": "yara", "content": "..." }
  ]
}

GET /api/rule/public/Convert_MISP

GET
/api/rule/public/Convert_MISP
Search rules and convert matching ones to MISP objects. Rules that cannot be converted return "misp_object": null.
ParameterTypeNotes
searchstringKeyword in rule title
authorstringFilter by author
rule_typestringFormat filter
sort_bystringnewest · oldest · most_likes · least_likes
curl
curl -G "http://127.0.0.1:7009/api/rule/public/Convert_MISP" \
  --data-urlencode "search=mars" \
  --data-urlencode "author=John" \
  --data-urlencode "rule_type=yara" \
  --data-urlencode "sort_by=newest"
response 200
{
  "total_rules_found": 1,
  "results": [
    {
      "uuid": "...", "title": "Mars_Ransomware", "format": "yara",
      "misp_object": { "name": "yara", "meta-category": "antivirus", ... }
    }
  ]
}

GET /api/rule/public/detail/<rule_id>

GET
/api/rule/public/detail/<rule_id>
Full detail of a single rule by its integer rule_id.
Path paramTypeDescription
rule_idintegerThe internal DB primary key of the rule
curl
curl http://127.0.0.1:7009/api/rule/public/detail/6
response 200
{
  "id": 6,
  "title": "Detect_Mimikatz",
  "format": "yara",
  "version": "1.0",
  "to_string": "rule Detect_Mimikatz { ... }",
  "description": "Detects Mimikatz credential dumper",
  "source": "internal",
  "license": "MIT",
  "cve_id": [],
  "original_uuid": null,
  "user": { "id": 2, "first_name": "Alice", "last_name": "Martin" }
}
Returns 404 if the rule or author does not exist.

GET /api/rule/public/all_by_user/<user_id>

GET
/api/rule/public/all_by_user/<user_id>
All rules created by a specific user. Returns an empty list (not 404) when the user has no rules.
curl
curl http://127.0.0.1:7009/api/rule/public/all_by_user/4
response 200
{
  "message": "3 rules found for user Alice Martin",
  "rules": [
    { "id": 6, "title": "...", "format": "yara", "version": "1.0",
      "to_string": "...", "description": "...", "source": "...",
      "license": "MIT", "cve_id": null, "original_uuid": null,
      "user": { "id": 4, "first_name": "Alice", "last_name": "Martin" } }
  ],
  "success": true
}

GET /api/rule/public/search_rules_by_cve

GET
/api/rule/public/search_rules_by_cve
Find rules linked to specific CVE / GHSA / PYSEC identifiers. The endpoint auto-detects and normalises vulnerability patterns from the raw input string.
ParameterTypeDescription
cve_idsstringComma-separated list or raw string with one or more IDs — e.g. CVE-2021-44228,GHSA-j8v8-6h6r-m6pq
curl
curl -G "http://127.0.0.1:7009/api/rule/public/search_rules_by_cve" \
  --data-urlencode "cve_ids=CVE-2021-44228,GHSA-j8v8-6h6r-m6pq"
response 200
{
  "detected_patterns": ["CVE-2021-44228", "GHSA-j8v8-6h6r-m6pq"],
  "total_matches": 5,
  "stats": { "yara": 3, "sigma": 2 },
  "results": [ { "uuid": "...", "title": "Log4Shell_Detect", ... } ]
}

Rule Private API

File: app/api/rule/rule_private_api.py — All endpoints require the X-API-KEY header decorated with @api_required.

GET /api/rule/private/me

GET
/api/rule/private/me
Test your API key. Returns the authenticated user's name and ID.
curl
curl http://127.0.0.1:7009/api/rule/private/me \
  -H "X-API-KEY: YOUR_KEY"
response 200
{ "message": "Welcome Alice!", "user_id": 4 }

POST /api/rule/private/create

POST
/api/rule/private/create
Create a new detection rule. Rule syntax is validated by the matching RuleType subclass before insertion.
Body accepts JSON (Content-Type: application/json) or URL query parameters. The author field is always set from the API key user and cannot be overridden.
FieldTypeRequiredConstraints / Notes
titlestringYesMust be unique and non-empty
formatstringYesyara · sigma · suricata · zeek · crs · nova · nse · wazuh · elastic
to_stringstringYesFull rule text. Must pass verify_syntax_rule_by_format()
versionstringYesNon-empty version string (e.g. "1.0")
licensestringYesNon-empty license string (e.g. "MIT")
descriptionstringNoDefaults to "No description provided"
sourcestringNoDefaults to authenticated user's full name
original_uuidstringNoPreserve original UUID from source system
cve_idstringNoCVE format: CVE-YYYY-NNNNN. Validated with utils.detect_cve()
curl
curl -X POST http://127.0.0.1:7009/api/rule/private/create \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: YOUR_KEY" \
  -d '{
    "title":       "Detect_Mimikatz_v2",
    "format":      "yara",
    "version":     "2.0",
    "license":     "MIT",
    "to_string":   "rule Detect_Mimikatz { strings: $s = \"sekurlsa\" condition: $s }",
    "description": "Detects Mimikatz sekurlsa module",
    "source":      "internal",
    "cve_id":      "CVE-2021-36934"
  }'
response 200
{ "message": "Rule created successfully", "rule": { "id": 99, "uuid": "...", "title": "Detect_Mimikatz_v2", ... } }
StatusCondition
200Rule created successfully
400Missing required fields, invalid CVE format, or syntax validation failure
409A rule with that title already exists

POST /api/rule/private/delete

POST
/api/rule/private/delete
Delete a rule by its integer ID. Only the rule owner or an admin can delete.
Field (JSON body)TypeRequiredNotes
rule_idintegerYesInternal DB primary key of the rule
curl
curl -X POST http://127.0.0.1:7009/api/rule/private/delete \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: YOUR_KEY" \
  -d '{"rule_id": 42}'
responses
200 → {"success": true,  "message": "Rule with ID '42' deleted successfully"}
403 → {"success": false, "message": "Access denied: you are not the owner or an admin"}
404 → {"success": false, "message": "No rule found with ID '42'"}

GET /api/rule/private/favorite/<rule_id>

GET
/api/rule/private/favorite/<rule_id>
Toggle a rule in/out of your favourites. Idempotent — calling twice returns the rule to its original state.
Path paramTypeDescription
rule_idintegerID of the rule to favourite / unfavourite
curl
curl http://127.0.0.1:7009/api/rule/private/favorite/4 \
  -H "X-API-KEY: YOUR_KEY"
responses
200 → {"success": true, "message": "Rule added to favorites"}
200 → {"success": true, "message": "Rule removed from favorites"}  # if already favorited
403 → {"success": false, "message": "Unauthorized"}

POST /api/rule/private/import_rules_from_github

POST
/api/rule/private/import_rules_from_github
Clone a GitHub repository and import all supported detection rules into the DB. Admin only.
This endpoint is restricted to admin users (user.is_admin). Non-admins receive a 400 error.
Field (JSON body)TypeRequiredNotes
urlstringYesValid GitHub repository URL (validated by valider_repo_github())
licensestringYesLicense to apply to all imported rules (e.g. "MIT")
curl
curl -X POST http://127.0.0.1:7009/api/rule/private/import_rules_from_github \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: ADMIN_KEY" \
  -d '{
    "url":     "https://github.com/org/yara-rules.git",
    "license": "MIT"
  }'
responses
200 → { "success": true, "imported": 183, "skipped": 12, "failed": 3 }
400 → { "success": false, "message": "You have to be an admin to import" }
400 → { "success": false, "message": "Invalid GitHub URL" }
500 → { "success": false, "message": "An error occurred while importing from: ...", "error": "..." }

POST /api/rule/private/dumpRules

POST
/api/rule/private/dumpRules
Full structured JSON dump of all rules for analysis or integration. All parameters are optional — pass an empty body {} to get everything.
Field (JSON body)TypeDescription
format_namestring or list"yara", ["yara","sigma"], or ["all"] for every format
created_afterstringISO date — "YYYY-MM-DD" or "YYYY-MM-DD HH:MM"
created_beforestringSame format
updated_afterstringSame format
updated_beforestringSame format
top_likedintegerReturn only the top N most liked rules
top_dislikedintegerReturn only the top N most disliked rules
curl — dump all rules
curl -X POST http://127.0.0.1:7009/api/rule/private/dumpRules \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: YOUR_KEY" \
  -d '{}'
curl — filtered dump
curl -X POST http://127.0.0.1:7009/api/rule/private/dumpRules \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: YOUR_KEY" \
  -d '{
    "format_name":    ["yara", "sigma"],
    "created_after":  "2025-01-01 00:00",
    "created_before": "2025-12-31 23:59",
    "top_liked":      10
  }'
response 200
{
  "success": true,
  "message": "Rules dump successfully generated.",
  "filters_applied": { "format_name": ["yara","sigma"], "top_liked": 10, ... },
  "data": {
    "summary_by_format": { "yara": 120, "sigma": 75, "total_rules": 195 },
    "rules": [
      {
        "id": 1, "title": "Example", "format": "yara", "author": "Alice",
        "created_at": "2025-03-01 12:00", "updated_at": "2025-04-10 09:30",
        "likes": 12, "dislikes": 0,
        "to_string": "rule Example { condition: true }",
        "cve_id": "CVE-2025-1234"
      }
    ]
  }
}
StatusCondition
200Dump generated
400Invalid JSON body or filter params
404No rules match the filters
403Missing/invalid API key

POST /api/rule/private/search

POST
/api/rule/private/search
Full-featured rule search identical to the web UI, with field selection, CVE filtering, tag filtering, and optional pagination. All body parameters are optional — combine with AND logic.
FieldTypeDefaultNotes
searchstringFree text across title, description, format, author, content, uuid
search_fieldstringallall · title · content
exact_matchbooleanfalseTitle uses strict equality; content uses case-sensitive LIKE
authorstringPartial, case-insensitive match
rule_typestringyara · sigma · suricata · zeek · crs · nova · nse · wazuh · elastic
sourcestringComma-separated for OR: "github.com,internal"
licensestringComma-separated for OR: "MIT,GPL"
vulnerabilitieslist[string]CVE IDs: ["CVE-2021-44228"] — validated as CVE-YYYY-NNNNN
tagslist[string]Tag names: ["malware","apt"]
sort_bystringnewestnewest · oldest · most_likes · least_likes
pageinteger1Must be ≥ 1
per_pageinteger201–100
paginatebooleantrueSet to false to get all results in one shot
fieldslist[string]allSubset of available fields (see below)
Available fields for fields param: id · title · format · author · license · description · version · source · uuid · original_uuid · creation_date · last_modif · vote_up · vote_down · to_string · cve_id · github_path
curl — typical search
curl -X POST http://127.0.0.1:7009/api/rule/private/search \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: YOUR_KEY" \
  -d '{
    "search":   "mimikatz",
    "rule_type":"yara",
    "tags":     ["credential_access"],
    "sort_by":  "most_likes",
    "page":     1,
    "per_page": 20
  }'
curl — select specific fields + no pagination
curl -X POST http://127.0.0.1:7009/api/rule/private/search \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: YOUR_KEY" \
  -d '{
    "rule_type": "sigma",
    "paginate":  false,
    "fields":    ["uuid","title","to_string","creation_date"]
  }'
response 200 (paginated)
{
  "success":  true,
  "total":    142,
  "page":     1,
  "per_page": 20,
  "pages":    8,
  "paginate": true,
  "rules": [
    { "uuid": "...", "title": "Mimikatz_Generic", "format": "yara",
      "author": "Alice", "vote_up": 15, "vote_down": 1, ... }
  ]
}

Bundle API

GET /api/bundle/public/search

File: app/api/bundle/bundle_public_api.py — No authentication required.

GET
/api/bundle/public/search
Search public bundles by name. Returns all matching bundles (no pagination).
ParameterTypeRequiredNotes
searchstringYesKeyword to match against bundle name. Empty value returns 400.
curl
curl -G "http://127.0.0.1:7009/api/bundle/public/search" \
  --data-urlencode "search=ransomware"
response 200
{
  "message": "2 bundle(s) found",
  "bundle_list": [
    { "id": 7, "name": "Ransomware Detections", "description": "...", "public": true, "score": 10 }
  ]
}

POST /api/bundle/private/create

File: app/api/bundle/bundle_private_api.py — Requires X-API-KEY.

POST
/api/bundle/private/create
Create a new bundle. The authenticated user becomes the owner.
Field (JSON body)TypeRequiredNotes
namestringYesNon-empty string
publicbooleanYesStrict boolean — must be true or false (not a string)
descriptionstringNoDefaults to empty string
curl
curl -X POST http://127.0.0.1:7009/api/bundle/private/create \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: YOUR_KEY" \
  -d '{
    "name":        "My YARA Bundle",
    "description": "Collection of YARA rules for ransomware detection",
    "public":      true
  }'
response 200
{ "message": "Bundle created successfully", "bundle_id": 12 }
StatusCondition
200Bundle created
400name missing/invalid, or public is not a boolean
500DB error

GET /api/bundle/private/add_rule_bundle

GET
/api/bundle/private/add_rule_bundle
Add a rule to an existing bundle. Only the bundle owner or an admin can perform this operation.
Parameter (query)TypeRequiredNotes
rule_idintegerYesID of the rule to add
bundle_idintegerYesID of the target bundle (must exist and be yours)
descriptionstringYesComment or note for this rule within the bundle
curl
curl "http://127.0.0.1:7009/api/bundle/private/add_rule_bundle?rule_id=42&bundle_id=7&description=Important+APT+rule" \
  -H "X-API-KEY: YOUR_KEY"
responses
200 → {"success": true,  "message": "Rule added!",                             "toast_class": "success"}
400 → {"success": false, "message": "Missing rule_id or bundle_id or description","toast_class": "danger"}
401 → {"success": false, "message": "You don't have the permission to do that!",  "toast_class": "danger"}
404 → {"success": false, "message": "Bundle not found",                           "toast_class": "danger"}

GET /api/bundle/private/remove_rule_bundle

GET
/api/bundle/private/remove_rule_bundle
Remove a rule from a bundle. Only the bundle owner or an admin can do this.
Parameter (query)TypeRequired
rule_idintegerYes
bundle_idintegerYes
curl
curl "http://127.0.0.1:7009/api/bundle/private/remove_rule_bundle?rule_id=42&bundle_id=7" \
  -H "X-API-KEY: YOUR_KEY"
responses
200 → {"success": true,  "message": "Rule removed!"}
401 → {"success": false, "message": "You don't have the permission to do that!"}
404 → {"success": false, "message": "Bundle not found"}
500 → {"success": false, "message": "Rule not found in this bundle or already removed"}

POST /api/bundle/private/edit_bundle/<bundle_id>

POST
/api/bundle/private/edit_bundle/<bundle_id>
Update a bundle's name and/or description. Only the owner or an admin.
Path paramTypeDescription
bundle_idintegerID of the bundle to update
Field (JSON body)TypeNotes
namestringNew bundle name (optional)
descriptionstringNew description (optional)
curl
curl -X POST http://127.0.0.1:7009/api/bundle/private/edit_bundle/7 \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: YOUR_KEY" \
  -d '{"name": "Updated Bundle Name", "description": "New description"}'
responses
200 → {"success": true,  "message": "Bundle updated successfully"}
401 → {"success": false, "message": "You don't have the permission to do that!"}
404 → {"success": false, "message": "Bundle not found"}

Account API

POST /api/account/public/register

File: app/api/account/account_public_api.py — No authentication required.

POST
/api/account/public/register
Create a new Rulezet account. Returns the new user's X-API-KEY on success.
Field (JSON body)TypeRequiredConstraints
emailstringYesValid email format, must be unique
passwordstringYes8–64 chars, at least 1 uppercase, 1 lowercase, 1 digit
first_namestringYesNon-empty
last_namestringYesNon-empty
curl
curl -X POST http://127.0.0.1:7009/api/account/public/register \
  -H "Content-Type: application/json" \
  -d '{
    "email":      "analyst@example.com",
    "password":   "Secur3Pass!",
    "first_name": "Alice",
    "last_name":  "Martin"
  }'
responses
201 → { "message": "User registered successfully", "X-API-KEY": "abc123...60chars..." }
400 → { "message": "Password must contain at least one digit." }
409 → { "message": "Email already exists" }

POST /api/account/public/login

POST
/api/account/public/login
Authenticate and create a Flask session. Used for browser-based flows; API integrations should use X-API-KEY instead.
FieldTypeRequiredNotes
emailstringYesRegistered email
passwordstringYesAccount password
remember_mebooleanNoExtends session — must be strict boolean
curl
curl -X POST http://127.0.0.1:7009/api/account/public/login \
  -H "Content-Type: application/json" \
  -d '{"email": "analyst@example.com", "password": "Secur3Pass!", "remember_me": false}'
responses
200 → { "message": "Logged in successfully" }
400 → { "message": "remember_me must be a boolean" }
401 → { "message": "Invalid email or password" }

POST /api/account/public/logout

POST
/api/account/public/logout
Terminate the current Flask session (@login_required — browser session must be active).
response 200
{ "message": "You have been logged out." }

POST /api/account/private/edit

File: app/api/account/account_private_api.py — Requires active login session (uses @login_required, not API key).

POST
/api/account/private/edit
Update the authenticated user's profile. Password update is optional — omit to keep current password.
Field (JSON body)TypeRequiredConstraints
first_namestringYesNon-empty
last_namestringYesNon-empty
emailstringYesValid email, unique
passwordstringNo8–64 chars, uppercase + lowercase + digit + special char (@$!%*?&)
curl
curl -X POST http://127.0.0.1:7009/api/account/private/edit \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: YOUR_KEY" \
  -d '{
    "first_name": "Alice",
    "last_name":  "Dupont",
    "email":      "alice@new.com",
    "password":   "NewP@ssw0rd!"
  }'
responses
200 → { "message": "User updated successfully" }
400 → { "message": "Password must contain at least one special character (@$!%*?&)" }
409 → { "message": "Email already registered." }

GET /api/account/private/favorite/get_rules_page_favorite

GET
/api/account/private/favorite/get_rules_page_favorite
Paginated list of the authenticated user's favourite rules.
Parameter (query)TypeDefault
pageinteger1
response 200
{ "rule": [ { "id": 6, "title": "...", ... } ], "total_pages": 3 }

POST /api/account/private/favorite/delete_rule

POST
/api/account/private/favorite/delete_rule
Remove a rule from your favourites list.
Parameter (query)TypeRequired
idintegerYes
curl
curl -X POST "http://127.0.0.1:7009/api/account/private/favorite/delete_rule?id=42" \
  -H "X-API-KEY: YOUR_KEY"
responses
200 → {"success": true,  "message": "Rule deleted!"}
403 → {"success": false, "message": "Access denied"}

Testing

Tests live in tests/ and use pytest + Flask's test client. The testing config uses SQLite so no PostgreSQL installation is needed.

conftest.py Fixtures

@pytest.fixture(scope="session")
def app() → Flask:
  # FLASKENV=testing, fresh SQLite DB, creates test users + rules
@pytest.fixture(scope="session")
def client(app) → FlaskClient:
  # app.test_client()

Running Tests

bash
# Full suite
./launch.sh -t

# Single test file
FLASKENV=testing pytest tests/test_rule_api.py -v

# Single test function
FLASKENV=testing pytest tests/test_rule_api.py::test_create_yara_rule -v

# With coverage
FLASKENV=testing pytest tests/ --cov=app --cov-report=html
Never run tests with FLASKENV=development — the test config disables CSRF and uses a throw-away SQLite database. The development PostgreSQL database must not be touched by the test suite.

Flask Blueprints

BlueprintURL PrefixFileResponsibility
home_bp/app/features/home/home.pyLanding page, search, feed
account_bp/accountapp/features/account/account.pyRegister, login, profile, settings, API key
rule_bp/ruleapp/features/rule/rule.pyRule CRUD, votes, comments, proposals
bundle_bp/bundleapp/features/bundle/bundle.pyBundle CRUD, export, votes, comments
tags_bp/tagsapp/features/tags/tags.pyTag management, bulk operations
jobs_bp/jobsapp/features/jobs/jobs.pyBackground job management UI
api_blueprint/apiapp/api/api.pyFlask-RESTX REST API + Swagger

Frontend — Vue.js 3

Vue.js 3 is loaded via a local static asset (not CDN). Templates use [[ ]] as delimiters (not {{ }}) to avoid conflict with Jinja2. Vue apps are ES-module scripts with type="module".

Rulezet v1.5.0 — Developer Documentation — Generated 2026-05-12