mirror of
https://github.com/LOLBAS-Project/LOLBAS
synced 2025-10-14 01:15:35 +02:00
121 lines
4.1 KiB
Python
121 lines
4.1 KiB
Python
import glob
|
|
import os
|
|
import sys
|
|
from typing import List, Literal, Optional
|
|
|
|
import yaml
|
|
from pydantic import BaseModel, HttpUrl, RootModel, ValidationError, constr, model_validator, field_validator, ConfigDict
|
|
|
|
# Disable datetime parsing
|
|
yaml.SafeLoader.yaml_implicit_resolvers = {k: [r for r in v if r[0] != 'tag:yaml.org,2002:timestamp'] for k, v in yaml.SafeLoader.yaml_implicit_resolvers.items()}
|
|
|
|
|
|
safe_str = constr(pattern=r'^([a-zA-Z0-9\s.,!?\'"():;\-\+_*#@/\\&%~=]|`[a-zA-Z0-9\s.,!?\'"():;\-\+_*#@/\\&<>%\{\}~=]+`|->)+$')
|
|
|
|
|
|
class LolbasModel(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
|
|
class AliasItem(LolbasModel):
|
|
Alias: Optional[str]
|
|
|
|
|
|
class TagItem(RootModel[dict[constr(pattern=r'^[A-Z]'), str]]):
|
|
pass
|
|
|
|
|
|
class CommandItem(LolbasModel):
|
|
Command: str
|
|
Description: safe_str
|
|
Usecase: safe_str
|
|
Category: Literal['ADS', 'AWL Bypass', 'Compile', 'Conceal', 'Copy', 'Credentials', 'Decode', 'Download', 'Dump', 'Encode', 'Execute', 'Reconnaissance', 'Tamper', 'UAC Bypass', 'Upload']
|
|
Privileges: str
|
|
MitreID: constr(pattern=r'^T[0-9]{4}(\.[0-9]{3})?$')
|
|
OperatingSystem: str
|
|
Tags: Optional[List[TagItem]] = None
|
|
|
|
|
|
class FullPathItem(LolbasModel):
|
|
Path: constr(pattern=r'^(([cC]:)\\([a-zA-Z0-9\-\_\. \(\)<>]+\\)*([a-zA-Z0-9_\-\.]+\.[a-z0-9]{3})|no default)$')
|
|
|
|
|
|
class CodeSampleItem(LolbasModel):
|
|
Code: str
|
|
|
|
|
|
class DetectionItem(LolbasModel):
|
|
IOC: Optional[str] = None
|
|
Sigma: Optional[HttpUrl] = None
|
|
Analysis: Optional[HttpUrl] = None
|
|
Elastic: Optional[HttpUrl] = None
|
|
Splunk: Optional[HttpUrl] = None
|
|
BlockRule: Optional[HttpUrl] = None
|
|
|
|
@model_validator(mode="after")
|
|
def validate_exclusive_urls(cls, values):
|
|
url_fields = ['IOC', 'Sigma', 'Analysis', 'Elastic', 'Splunk', 'BlockRule']
|
|
present = [field for field in url_fields if values.__dict__.get(field) is not None]
|
|
|
|
if len(present) != 1:
|
|
raise ValueError(f"Exactly one of the following must be provided: {url_fields}.", f"Currently set: {present or 'none'}")
|
|
|
|
return values
|
|
|
|
|
|
class ResourceItem(LolbasModel):
|
|
Link: HttpUrl
|
|
|
|
|
|
class AcknowledgementItem(LolbasModel):
|
|
Person: str
|
|
Handle: Optional[constr(pattern=r'^(@(\w){1,15})?$')] = None
|
|
|
|
|
|
class MainModel(LolbasModel):
|
|
Name: str
|
|
Description: safe_str
|
|
Aliases: Optional[List[AliasItem]] = None
|
|
Author: str
|
|
Created: constr(pattern=r'\d{4}-\d{2}-\d{2}')
|
|
Commands: List[CommandItem]
|
|
Full_Path: List[FullPathItem]
|
|
Code_Sample: Optional[List[CodeSampleItem]] = None
|
|
Detection: Optional[List[DetectionItem]] = None
|
|
Resources: Optional[List[ResourceItem]] = None
|
|
Acknowledgement: Optional[List[AcknowledgementItem]] = None
|
|
|
|
|
|
if __name__ == "__main__":
|
|
def escaper(x): return x.replace('%', '%25').replace('\r', '%0D').replace('\n', '%0A')
|
|
|
|
yaml_files = glob.glob("yml/**", recursive=True)
|
|
|
|
if not yaml_files:
|
|
print("No YAML files found under 'yml/**'.")
|
|
sys.exit(-1)
|
|
|
|
has_errors = False
|
|
for file_path in yaml_files:
|
|
if os.path.isfile(file_path) and not file_path.startswith('yml/HonorableMentions/'):
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
data = yaml.safe_load(f)
|
|
MainModel(**data)
|
|
print(f"✅ Valid: {file_path}")
|
|
except ValidationError as ve:
|
|
print(f"❌ Validation error in {file_path}:\n{ve}\n")
|
|
for err in ve.errors():
|
|
# GitHub Actions error format
|
|
print(err)
|
|
path = '.'.join([str(x) for x in err.get('loc', [None])])
|
|
msg = err.get('msg', 'Unknown validation error')
|
|
print(f"::error file={file_path},line=1,title={escaper(err.get('type') or 'Validation error')}::{escaper(msg)}: {escaper(path)}")
|
|
has_errors = True
|
|
except Exception as e:
|
|
print(f"⚠️ Error processing {file_path}: {e}\n")
|
|
print(f"::error file={file_path},line=1,title=Processing error::Error processing file: {escaper(e)}")
|
|
has_errors = True
|
|
|
|
sys.exit(-1 if has_errors else 0)
|