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.
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.
app.py— CLI runner, DB managementlaunch.sh— dev/test/init helperapp/__init__.py—create_app()factoryconfig.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.
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
| Extension | Object | Purpose |
|---|---|---|
Flask-SQLAlchemy | db | ORM, session management |
Flask-Migrate | migrate | Alembic-based schema migrations |
Flask-Login | login_manager | Session auth, current_user |
Flask-WTF / CSRFProtect | csrf | CSRF tokens (exempted for API) |
Flask-Session | sess | Server-side sessions (SQLAlchemy or filesystem) |
Flask-Mail | mail | Email notifications |
Flask-RESTX | api | REST 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 value | Class | Database | Session | Notes |
|---|---|---|---|---|
development (default) | DevelopmentConfig | postgresql:///rulezet | SQLAlchemy | DEBUG=True |
testing | TestingConfig | sqlite:///rulezet-test.sqlite | filesystem | CSRF disabled, TESTING=True |
production | ProductionConfig | postgresql:///rulezet | SQLAlchemy | DEBUG=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
./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
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
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
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.
before_flush SQLAlchemy event listener (receive_before_flush) auto-updates Gamification.score and level whenever a relevant model is inserted or deleted.User & Auth
- 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
- 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
- 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
| Model | Key Fields | Purpose |
|---|---|---|
RuleVote | user_id, rule_id, value (+1/-1) | Upvote/downvote a rule |
RuleFavoriteUser | user_id, rule_id | Bookmark a rule |
Comment | user_id, rule_id, content, parent_id | Threaded comments on rules |
RepportRule | user_id, rule_id, reason | Flag a rule for review |
RequestOwnerRule | user_id, rule_id, status | Claim authorship of a rule |
RuleEditProposal | rule_id, proposed_content, status | Community edit suggestion |
RuleEditComment | proposal_id, user_id, content | Comments on edit proposals |
RuleEditContribution | proposal_id, user_id | Track contributors of an edit |
RuleUpdateHistory | rule_id, old_content, new_content, updated_at | Diff history for a rule |
NewRule | user_id, format, content | Pending rule submission queue |
RuleStatus | rule_id, status, reason | Review/approval status |
Tags
- idInteger PK
- nameStringUnique tag label
- colorStringHex colour for badge
- created_byFK → User
- rule_idFK → Rule
- tag_idFK → Tag
Bundles
| Model | Key Fields | Purpose |
|---|---|---|
Bundle | id, uuid, name, description, user_id, is_public, score | Named collection of rules |
BundleNode | bundle_id, rule_id, position | Ordered rule within a bundle |
BundleRuleAssociation | bundle_id, rule_id | Many-to-many bridge |
BundleTagAssociation | bundle_id, tag_id | Tags on a bundle |
BundleVote | user_id, bundle_id, value | Vote on a bundle |
CommentBundle | user_id, bundle_id, content | Comments on a bundle |
BundleReactionComment | comment_id, user_id, reaction | Emoji reactions on bundle comments |
GitHub & Import
| Model | Key Fields | Purpose |
|---|---|---|
ImporterResult | uuid, info (JSON), bad_rules, imported, skipped, total, count_per_format (JSON), query_date, user_id | Persisted result of a GitHub import session |
UpdateResult | uuid, info (JSON), updated, skipped, errors, query_date, user_id | Persisted result of a GitHub update session |
Background Jobs
| Model | Key Fields | Purpose |
|---|---|---|
BackgroundJob | uuid, job_type, status (pending/running/done/failed/cancelled/paused), payload (JSON), label, created_by (FK→User), created_at, started_at, finished_at | Persistent job record |
BackgroundJobLog | job_id (FK→BackgroundJob), message, level, created_at | Log entries for a job |
Similarity
| Model | Key Fields | Purpose |
|---|---|---|
RuleSimilarity | rule_id_a, rule_id_b, score (float), method | Stores pre-computed similarity pairs |
SimilarResult | session_uuid, rule_id, similar_rule_id, score | Per-session similarity results |
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
Level Thresholds (LEVEL_THRESHOLDS)
| Level | Min Score |
|---|---|
| 1 | 0 |
| 2 | 500 |
| 3 | 15 000 |
Event Listener
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 / Property | Signature | Purpose |
|---|---|---|
format | @property → str | Format identifier (e.g. "yara") |
get_class() | → str | Human-readable class name |
validate(content) | str → ValidationResult | Parse + validate raw rule text |
parse_metadata(content, info, validation_result) | str, dict, ValidationResult → dict | Extract structured metadata dict from rule |
get_rule_files(file) | str → bool | Whether 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
def load_all_rule_formats() → None:
# Uses pkgutil.iter_modules on available_format/ package
# Imports each module so RuleType.__subclasses__() is populated
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(). Onundefined identifiererrors, automatically prependsimport "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
| Format | Class | Library | Extensions |
|---|---|---|---|
| Sigma | SigmaRule | pySigma | .yml, .yaml |
| Suricata | SuricataRule | suricataparser, idstools | .rules |
| Zeek | ZeekRule | zeekscript | .zeek |
| CRS | CRSRule | msc_pyparser | .conf |
| Nova | NovaRule | nova-hunting | .nova |
| NSE | NSERule | Lua parser | .nse |
| Wazuh | WazuhRule | XML/YAML | .xml, .yml |
| Elastic | ElasticRule | JSON/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
# Inserts Rule into DB; checks duplicates, resolves FormatRule, assigns tags
# Updates rule, creates RuleUpdateHistory entry
# value must be +1 or -1; creates/updates/removes RuleVote
main_format.py — Format Orchestration
format_files, format_rule, info, format_name, user
) → tuple[int, int, int] # (bad_rules, imported, skipped)
# Async; walks repo_dir, dispatches to each RuleType subclass
rule_content: str, user: User, format_name: str,
url_repo: str, github_path: 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
user: User,
info: dict,
mode: str = "global", # "global" | "targeted"
target_rule_id: Optional[int] = None,
params: Optional[dict] = None
)
| Step | Tool | Detail |
|---|---|---|
| 1. Vectorise | sklearn TfidfVectorizer | Converts rule content to TF-IDF matrix |
| 2. ANN search | faiss-cpu IndexFlatIP | Fast approximate nearest-neighbour lookup |
| 3. Fuzzy score | rapidfuzz.fuzz.ratio | Character-level ratio, returns 0.0–1.0 |
| 4. Persist | SQLAlchemy | Results stored in RuleSimilarity |
# 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.
# uuid, thread_count=4, jobs Queue, repo_dir, counts (bad/imported/skipped)
| Method | Description |
|---|---|
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 |
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.
repo_sources: list,
user: User,
info: dict,
mode: str # "by_url" | "by_rule"
)
| Mode | Behaviour |
|---|---|
by_url | Re-clones the source repo and re-processes all rules |
by_rule | Finds 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
# Decorator — registers a callable into _HANDLERS dict
# Usage: @register_handler('bulk_add_tag_to_rules')
# Called by create_app(). Starts daemon thread → _worker_loop(app)
# 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
| Function | Signature |
|---|---|
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 Type | Handler Function | Description |
|---|---|---|
bulk_add_tag_to_rules | handle_bulk_add_tag_to_rules | Adds a tag to all rules matching a filter set |
bulk_remove_tag_from_rules | handle_bulk_remove_tag_from_rules | Removes a tag from filtered rules |
delete_github_rules | handle_delete_github_rules | Deletes all rules originating from a given GitHub source |
_build_rule_query(payload) mirrors the exact filter params from the UI search.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 label | URL prefix | Auth | Source file |
|---|---|---|---|
| Public action on Rule ✅ | /api/rule/public | None | app/api/rule/rule_public_api.py |
| Private action on Rule 🔑 | /api/rule/private | X-API-KEY | app/api/rule/rule_private_api.py |
| Public action on Bundle ✅ | /api/bundle/public | None | app/api/bundle/bundle_public_api.py |
| Private action on Bundle 🔑 | /api/bundle/private | X-API-KEY | app/api/bundle/bundle_private_api.py |
| Public account action ✅ | /api/account/public | None | app/api/account/account_public_api.py |
| Private account action 🔑 | /api/account/private | X-API-KEY | app/api/account/account_private_api.py |
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
# Cryptographically-random alphanumeric key (secrets.token_hex based)
# Returns True if X-API-KEY header is present and matches a user row
# 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.
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
| Status | Condition | Body |
|---|---|---|
401 | Missing or invalid X-API-KEY | {"message": "Unauthorized"} |
403 | Key valid but not enough permissions | {"success": false, "message": "Access denied"} |
Quick test
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
| Parameter | Type | Default | Allowed values / notes |
|---|---|---|---|
search | string | — | Keyword to match against rule title |
author | string | — | Filter by author username |
rule_type | string | — | yara, sigma, suricata, zeek, crs, nova, nse, wazuh, elastic |
sort_by | string | — | newest · oldest · most_likes · least_likes |
page | integer | 1 | 1-based page number |
per_page | integer | 10 | Items per page |
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"
{
"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
searchPage but returns all matching rules in a single response (no pagination).| Parameter | Type | Allowed values |
|---|---|---|
search | string | Free text |
author | string | Author username |
rule_type | string | yara · sigma · suricata · zeek · crs · nova · nse · wazuh · elastic |
sort_by | string | newest · oldest · most_likes · least_likes |
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"
{
"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
"misp_object": null.| Parameter | Type | Notes |
|---|---|---|
search | string | Keyword in rule title |
author | string | Filter by author |
rule_type | string | Format filter |
sort_by | string | newest · oldest · most_likes · least_likes |
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"
{
"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>
rule_id.| Path param | Type | Description |
|---|---|---|
rule_id | integer | The internal DB primary key of the rule |
curl http://127.0.0.1:7009/api/rule/public/detail/6
{
"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" }
}
404 if the rule or author does not exist.GET /api/rule/public/all_by_user/<user_id>
curl http://127.0.0.1:7009/api/rule/public/all_by_user/4
{
"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
| Parameter | Type | Description |
|---|---|---|
cve_ids | string | Comma-separated list or raw string with one or more IDs — e.g. CVE-2021-44228,GHSA-j8v8-6h6r-m6pq |
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"
{
"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
curl http://127.0.0.1:7009/api/rule/private/me \
-H "X-API-KEY: YOUR_KEY"
{ "message": "Welcome Alice!", "user_id": 4 }
POST /api/rule/private/create
RuleType subclass before insertion.Content-Type: application/json) or URL query parameters. The author field is always set from the API key user and cannot be overridden.| Field | Type | Required | Constraints / Notes |
|---|---|---|---|
title | string | Yes | Must be unique and non-empty |
format | string | Yes | yara · sigma · suricata · zeek · crs · nova · nse · wazuh · elastic |
to_string | string | Yes | Full rule text. Must pass verify_syntax_rule_by_format() |
version | string | Yes | Non-empty version string (e.g. "1.0") |
license | string | Yes | Non-empty license string (e.g. "MIT") |
description | string | No | Defaults to "No description provided" |
source | string | No | Defaults to authenticated user's full name |
original_uuid | string | No | Preserve original UUID from source system |
cve_id | string | No | CVE format: CVE-YYYY-NNNNN. Validated with utils.detect_cve() |
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"
}'
{ "message": "Rule created successfully", "rule": { "id": 99, "uuid": "...", "title": "Detect_Mimikatz_v2", ... } }
| Status | Condition |
|---|---|
200 | Rule created successfully |
400 | Missing required fields, invalid CVE format, or syntax validation failure |
409 | A rule with that title already exists |
POST /api/rule/private/delete
| Field (JSON body) | Type | Required | Notes |
|---|---|---|---|
rule_id | integer | Yes | Internal DB primary key of the rule |
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}'
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>
| Path param | Type | Description |
|---|---|---|
rule_id | integer | ID of the rule to favourite / unfavourite |
curl http://127.0.0.1:7009/api/rule/private/favorite/4 \
-H "X-API-KEY: YOUR_KEY"
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
user.is_admin). Non-admins receive a 400 error.| Field (JSON body) | Type | Required | Notes |
|---|---|---|---|
url | string | Yes | Valid GitHub repository URL (validated by valider_repo_github()) |
license | string | Yes | License to apply to all imported rules (e.g. "MIT") |
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"
}'
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
{} to get everything.| Field (JSON body) | Type | Description |
|---|---|---|
format_name | string or list | "yara", ["yara","sigma"], or ["all"] for every format |
created_after | string | ISO date — "YYYY-MM-DD" or "YYYY-MM-DD HH:MM" |
created_before | string | Same format |
updated_after | string | Same format |
updated_before | string | Same format |
top_liked | integer | Return only the top N most liked rules |
top_disliked | integer | Return only the top N most disliked 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 -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
}'
{
"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"
}
]
}
}
| Status | Condition |
|---|---|
200 | Dump generated |
400 | Invalid JSON body or filter params |
404 | No rules match the filters |
403 | Missing/invalid API key |
POST /api/rule/private/search
| Field | Type | Default | Notes |
|---|---|---|---|
search | string | — | Free text across title, description, format, author, content, uuid |
search_field | string | all | all · title · content |
exact_match | boolean | false | Title uses strict equality; content uses case-sensitive LIKE |
author | string | — | Partial, case-insensitive match |
rule_type | string | — | yara · sigma · suricata · zeek · crs · nova · nse · wazuh · elastic |
source | string | — | Comma-separated for OR: "github.com,internal" |
license | string | — | Comma-separated for OR: "MIT,GPL" |
vulnerabilities | list[string] | — | CVE IDs: ["CVE-2021-44228"] — validated as CVE-YYYY-NNNNN |
tags | list[string] | — | Tag names: ["malware","apt"] |
sort_by | string | newest | newest · oldest · most_likes · least_likes |
page | integer | 1 | Must be ≥ 1 |
per_page | integer | 20 | 1–100 |
paginate | boolean | true | Set to false to get all results in one shot |
fields | list[string] | all | Subset of available fields (see below) |
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_pathcurl -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 -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"]
}'
{
"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.
| Parameter | Type | Required | Notes |
|---|---|---|---|
search | string | Yes | Keyword to match against bundle name. Empty value returns 400. |
curl -G "http://127.0.0.1:7009/api/bundle/public/search" \
--data-urlencode "search=ransomware"
{
"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.
| Field (JSON body) | Type | Required | Notes |
|---|---|---|---|
name | string | Yes | Non-empty string |
public | boolean | Yes | Strict boolean — must be true or false (not a string) |
description | string | No | Defaults to empty string |
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
}'
{ "message": "Bundle created successfully", "bundle_id": 12 }
| Status | Condition |
|---|---|
200 | Bundle created |
400 | name missing/invalid, or public is not a boolean |
500 | DB error |
GET /api/bundle/private/add_rule_bundle
| Parameter (query) | Type | Required | Notes |
|---|---|---|---|
rule_id | integer | Yes | ID of the rule to add |
bundle_id | integer | Yes | ID of the target bundle (must exist and be yours) |
description | string | Yes | Comment or note for this rule within the bundle |
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"
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
| Parameter (query) | Type | Required |
|---|---|---|
rule_id | integer | Yes |
bundle_id | integer | Yes |
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"
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>
| Path param | Type | Description |
|---|---|---|
bundle_id | integer | ID of the bundle to update |
| Field (JSON body) | Type | Notes |
|---|---|---|
name | string | New bundle name (optional) |
description | string | New description (optional) |
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"}'
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.
X-API-KEY on success.| Field (JSON body) | Type | Required | Constraints |
|---|---|---|---|
email | string | Yes | Valid email format, must be unique |
password | string | Yes | 8–64 chars, at least 1 uppercase, 1 lowercase, 1 digit |
first_name | string | Yes | Non-empty |
last_name | string | Yes | Non-empty |
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"
}'
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
X-API-KEY instead.| Field | Type | Required | Notes |
|---|---|---|---|
email | string | Yes | Registered email |
password | string | Yes | Account password |
remember_me | boolean | No | Extends session — must be strict boolean |
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}'
200 → { "message": "Logged in successfully" }
400 → { "message": "remember_me must be a boolean" }
401 → { "message": "Invalid email or password" }
POST /api/account/public/logout
@login_required — browser session must be active).{ "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).
| Field (JSON body) | Type | Required | Constraints |
|---|---|---|---|
first_name | string | Yes | Non-empty |
last_name | string | Yes | Non-empty |
email | string | Yes | Valid email, unique |
password | string | No | 8–64 chars, uppercase + lowercase + digit + special char (@$!%*?&) |
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!"
}'
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
| Parameter (query) | Type | Default |
|---|---|---|
page | integer | 1 |
{ "rule": [ { "id": 6, "title": "...", ... } ], "total_pages": 3 }
POST /api/account/private/favorite/delete_rule
| Parameter (query) | Type | Required |
|---|---|---|
id | integer | Yes |
curl -X POST "http://127.0.0.1:7009/api/account/private/favorite/delete_rule?id=42" \
-H "X-API-KEY: YOUR_KEY"
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
def app() → Flask:
# FLASKENV=testing, fresh SQLite DB, creates test users + rules
def client(app) → FlaskClient:
# app.test_client()
Running Tests
# 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
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
| Blueprint | URL Prefix | File | Responsibility |
|---|---|---|---|
home_bp | / | app/features/home/home.py | Landing page, search, feed |
account_bp | /account | app/features/account/account.py | Register, login, profile, settings, API key |
rule_bp | /rule | app/features/rule/rule.py | Rule CRUD, votes, comments, proposals |
bundle_bp | /bundle | app/features/bundle/bundle.py | Bundle CRUD, export, votes, comments |
tags_bp | /tags | app/features/tags/tags.py | Tag management, bulk operations |
jobs_bp | /jobs | app/features/jobs/jobs.py | Background job management UI |
api_blueprint | /api | app/api/api.py | Flask-RESTX REST API + Swagger |
Frontend — Vue.js 3
[[ ]] as delimiters (not {{ }}) to avoid conflict with Jinja2. Vue apps are ES-module scripts with type="module".