Skip to content

Commit 41d45e4

Browse files
committed
New pydantic schema checker
1 parent 3655c57 commit 41d45e4

File tree

3 files changed

+116
-157
lines changed

3 files changed

+116
-157
lines changed

.github/workflows/validation.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import glob
2+
import os
3+
import sys
4+
from datetime import date
5+
from typing import List, Literal, Optional
6+
7+
import yaml
8+
from pydantic import (BaseModel, Field, HttpUrl, RootModel, ValidationError,
9+
constr, model_validator)
10+
11+
safe_str = constr(pattern=r'^([a-zA-Z0-9\s.,!?\'"():;\-\+_*#@/\\&%~=]|`[a-zA-Z0-9\s.,!?\'"():;\-\+_*#@/\\&<>%\{\}~=]+`|->)+$')
12+
13+
14+
class AliasItem(BaseModel):
15+
Alias: Optional[str]
16+
17+
18+
class TagItem(RootModel[dict[constr(pattern=r'^[A-Z]'), str]]):
19+
pass
20+
21+
22+
class CommandItem(BaseModel):
23+
Command: str
24+
Description: safe_str
25+
Usecase: safe_str
26+
Category: Literal['ADS', 'AWL Bypass', 'Compile', 'Conceal', 'Copy', 'Credentials', 'Decode', 'Download', 'Dump', 'Encode', 'Execute', 'Reconnaissance', 'Tamper', 'UAC Bypass', 'Upload']
27+
Privileges: str
28+
MitreID: constr(pattern=r'^T[0-9]{4}(\.[0-9]{3})?$')
29+
OperatingSystem: str
30+
Tags: Optional[List[TagItem]] = None
31+
32+
33+
class FullPathItem(BaseModel):
34+
Path: constr(pattern=r'^(([cC]:)\\([a-zA-Z0-9\-\_\. \(\)<>]+\\)*([a-zA-Z0-9_\-\.]+\.[a-z0-9]{3})|no default)$')
35+
36+
37+
class CodeSampleItem(BaseModel):
38+
Code: str
39+
40+
41+
class DetectionItem(BaseModel):
42+
IOC: Optional[str] = None
43+
Sigma: Optional[HttpUrl] = None
44+
Analysis: Optional[HttpUrl] = None
45+
Elastic: Optional[HttpUrl] = None
46+
Splunk: Optional[HttpUrl] = None
47+
BlockRule: Optional[HttpUrl] = None
48+
49+
@model_validator(mode="after")
50+
def validate_exclusive_urls(cls, values):
51+
url_fields = ['IOC', 'Sigma', 'Analysis', 'Elastic', 'Splunk', 'BlockRule']
52+
present = [field for field in url_fields if values.__dict__.get(field) is not None]
53+
54+
if len(present) != 1:
55+
raise ValueError(
56+
f"Exactly one of the following must be provided: {url_fields}. "
57+
f"Currently set: {present or 'none'}"
58+
)
59+
60+
return values
61+
62+
class ResourceItem(BaseModel):
63+
Link: HttpUrl
64+
65+
66+
class AcknowledgementItem(BaseModel):
67+
Person: str
68+
Handle: Optional[constr(pattern=r'^(@(\w){1,15})?$')] = None
69+
70+
71+
class MainModel(BaseModel):
72+
Name: str
73+
Description: str
74+
Aliases: Optional[List[AliasItem]] = None
75+
Author: str
76+
Created: date
77+
Commands: List[CommandItem]
78+
Full_Path: List[FullPathItem]
79+
Code_Sample: Optional[List[CodeSampleItem]] = None
80+
Detection: Optional[List[DetectionItem]] = None
81+
Resources: Optional[List[ResourceItem]] = None
82+
Acknowledgement: Optional[List[AcknowledgementItem]] = None
83+
84+
85+
if __name__ == "__main__":
86+
87+
yaml_files = glob.glob("yml/**", recursive=True)
88+
89+
if not yaml_files:
90+
print("No YAML files found under 'yml/**'.")
91+
sys.exit(0)
92+
93+
for file_path in yaml_files:
94+
if os.path.isfile(file_path) and not file_path.startswith('yml/HonorableMentions/'):
95+
try:
96+
with open(file_path, 'r', encoding='utf-8') as f:
97+
data = yaml.safe_load(f)
98+
MainModel(**data)
99+
print(f"✅ Valid: {file_path}")
100+
except ValidationError as ve:
101+
print(f"❌ Validation error in {file_path}:\n{ve}\n")
102+
for err in ve.errors():
103+
# GitHub Actions error format
104+
path = '.'.join([str(x) for x in err.get('loc', [None])])
105+
msg = err.get('msg', 'Unknown validation error')
106+
print(f"::error file={file_path},line=1,col=1::'{msg}' for {path}")
107+
except Exception as e:
108+
print(f"⚠️ Error processing {file_path}: {e}\n")
109+
print(f"::error file={file_path},line=1,col=1::Error processing file: {e}")

.github/workflows/yaml-linting.yml

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
---
22
name: PUSH & PULL REQUEST - YAML Lint and Schema Validation Checks
3-
on: [push,pull_request]
3+
on: [push, pull_request]
44

55
jobs:
66
lintFiles:
77
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
88
runs-on: ubuntu-latest
99
steps:
1010
- uses: actions/checkout@v3
11+
1112
- name: Check file extensions
1213
run: |
1314
files=$(find "$GITHUB_WORKSPACE/yml" -type f -not -name "*.yml");
@@ -17,6 +18,7 @@ jobs:
1718
exit 1;
1819
fi
1920
unset files
21+
2022
- name: Check duplicate file names
2123
run: |
2224
files=$(find "$GITHUB_WORKSPACE/yml/OSBinaries" "$GITHUB_WORKSPACE/yml/OtherMSBinaries" -type f -printf '%h %f\n' -iname "*.yml" | sort -t ' ' -k 2,2 -f | uniq -i -f 1 --all-repeated=separate | tr ' ' '/')
@@ -27,34 +29,11 @@ jobs:
2729
fi
2830
unset files
2931
30-
- name: Install yamllint
31-
run: pip install yamllint
32+
- name: Install python dependencies
33+
run: pip install yamllint==1.37.1 pydantic==2.11.9
3234

3335
- name: Lint YAML files
3436
run: yamllint -c .github/.yamllint yml/**/
3537

36-
- name: Validate Template Schema
37-
uses: cketti/[email protected]
38-
with:
39-
files: YML-Template.yml
40-
schema: YML-Schema.yml
41-
- name: Validate OSBinaries YAML Schema
42-
uses: cketti/[email protected]
43-
with:
44-
files: yml/OSBinaries/*.yml
45-
schema: YML-Schema.yml
46-
- name: Validate OSLibraries YAML Schema
47-
uses: cketti/[email protected]
48-
with:
49-
files: yml/OSLibraries/*.yml
50-
schema: YML-Schema.yml
51-
- name: Validate OSScripts YAML Schema
52-
uses: cketti/[email protected]
53-
with:
54-
files: yml/OSScripts/*.yml
55-
schema: YML-Schema.yml
56-
- name: Validate OtherMSBinaries YAML Schema
57-
uses: cketti/[email protected]
58-
with:
59-
files: yml/OtherMSBinaries/*.yml
60-
schema: YML-Schema.yml
38+
- name: Validate YAML schemas
39+
run: python3 .github/workflows/validation.py

YML-Schema.yml

Lines changed: 0 additions & 129 deletions
This file was deleted.

0 commit comments

Comments
 (0)