Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions sources/core/codeguard-0-additional-cryptography.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ languages:
- typescript
- xml
- yaml
tags:
- data-security
- secrets
alwaysApply: false
---

Expand Down
2 changes: 2 additions & 0 deletions sources/core/codeguard-0-api-web-services.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ languages:
- typescript
- xml
- yaml
tags:
- web
alwaysApply: false
---

Expand Down
3 changes: 3 additions & 0 deletions sources/core/codeguard-0-authentication-mfa.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ languages:
- ruby
- swift
- typescript
tags:
- authentication
- web
alwaysApply: false
---

Expand Down
2 changes: 2 additions & 0 deletions sources/core/codeguard-0-client-side-web-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ languages:
- php
- typescript
- vlang
tags:
- web
alwaysApply: false
---

Expand Down
2 changes: 2 additions & 0 deletions sources/core/codeguard-0-cloud-orchestration-kubernetes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ description: Kubernetes hardening (RBAC, admission policies, network policies, s
languages:
- javascript
- yaml
tags:
- infrastructure
alwaysApply: false
---

Expand Down
3 changes: 3 additions & 0 deletions sources/core/codeguard-0-data-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ languages:
- javascript
- sql
- yaml
tags:
- data-security
- infrastructure
alwaysApply: false
---

Expand Down
2 changes: 2 additions & 0 deletions sources/core/codeguard-0-devops-ci-cd-containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ languages:
- shell
- xml
- yaml
tags:
- infrastructure
alwaysApply: false
---

Expand Down
2 changes: 2 additions & 0 deletions sources/core/codeguard-0-iac-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ languages:
- ruby
- shell
- yaml
tags:
- infrastructure
alwaysApply: false
---

Expand Down
2 changes: 2 additions & 0 deletions sources/core/codeguard-0-input-validation-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ languages:
- shell
- sql
- typescript
tags:
- web
alwaysApply: false
---

Expand Down
2 changes: 2 additions & 0 deletions sources/core/codeguard-0-logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ languages:
- c
- javascript
- yaml
tags:
- privacy
alwaysApply: false
---

Expand Down
2 changes: 2 additions & 0 deletions sources/core/codeguard-0-privacy-data-protection.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ languages:
- javascript
- matlab
- yaml
tags:
- privacy
alwaysApply: false
---

Expand Down
3 changes: 3 additions & 0 deletions sources/core/codeguard-0-session-management-and-cookies.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ languages:
- python
- ruby
- typescript
tags:
- authentication
- web
alwaysApply: false
---

Expand Down
2 changes: 2 additions & 0 deletions sources/core/codeguard-1-digital-certificates.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
description: Certificate Best Practices
languages: []
tags:
- secrets
alwaysApply: true
---

Expand Down
2 changes: 2 additions & 0 deletions sources/core/codeguard-1-hardcoded-credentials.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
description: No Hardcoded Credentials
languages: []
tags:
- secrets
alwaysApply: true
---

Expand Down
60 changes: 53 additions & 7 deletions src/convert_to_ide_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,23 @@ def sync_plugin_metadata(version: str) -> None:
print(f"✅ Synced plugin metadata to {version}")


def matches_tag_filter(rule_tags: list[str], filter_tags: list[str]) -> bool:
"""
Check if rule has all required tags (AND logic).

Args:
rule_tags: List of tags from the rule (already normalized to lowercase)
filter_tags: List of tags to filter by (already normalized to lowercase)

Returns:
True if rule has all filter tags (or no filter), False otherwise
"""
if not filter_tags:
return True # No filter means all pass

return all(tag in rule_tags for tag in filter_tags)


def update_skill_md(language_to_rules: dict[str, list[str]], skill_path: str) -> None:
"""
Update SKILL.md with language-to-rules mapping table.
Expand Down Expand Up @@ -81,7 +98,7 @@ def update_skill_md(language_to_rules: dict[str, list[str]], skill_path: str) ->
print(f"Updated SKILL.md with language mappings")


def convert_rules(input_path: str, output_dir: str = "dist", include_claudecode: bool = True, version: str = None) -> dict[str, list[str]]:
def convert_rules(input_path: str, output_dir: str = "dist", include_claudecode: bool = True, version: str = None, filter_tags: list[str] = None) -> dict[str, list[str]]:
"""
Convert rule file(s) to all supported IDE formats using RuleConverter.

Expand All @@ -90,6 +107,7 @@ def convert_rules(input_path: str, output_dir: str = "dist", include_claudecode:
output_dir: Output directory (default: 'dist/')
include_claudecode: Whether to generate Claude Code plugin (default: True, only for core rules)
version: Version string to use (default: read from pyproject.toml)
filter_tags: Optional list of tags to filter by (AND logic, case-insensitive)

Returns:
Dictionary with 'success' and 'errors' lists:
Expand Down Expand Up @@ -138,14 +156,19 @@ def convert_rules(input_path: str, output_dir: str = "dist", include_claudecode:
# Setup output directory
output_base = Path(output_dir)

results = {"success": [], "errors": []}
results = {"success": [], "errors": [], "skipped": []}
language_to_rules = defaultdict(list)

# Process each file
for md_file in md_files:
try:
# Convert the file (raises exceptions on error)
result = converter.convert(md_file)

# Apply tag filter if specified
if filter_tags and not matches_tag_filter(result.tags, filter_tags):
results["skipped"].append(result.filename)
continue

# Write each format
output_files = []
Expand Down Expand Up @@ -192,9 +215,14 @@ def convert_rules(input_path: str, output_dir: str = "dist", include_claudecode:
results["errors"].append(error_msg)

# Summary
print(
f"\nResults: {len(results['success'])} success, {len(results['errors'])} errors"
)
if filter_tags:
print(
f"\nResults: {len(results['success'])} success, {len(results['skipped'])} skipped (tag filter), {len(results['errors'])} errors"
)
else:
print(
f"\nResults: {len(results['success'])} success, {len(results['errors'])} errors"
)

# Generate SKILL.md with language mappings (only if Claude Code is included)
if include_claudecode and language_to_rules:
Expand Down Expand Up @@ -256,6 +284,12 @@ def _resolve_source_paths(args) -> list[Path]:
default="dist",
help="Output directory for generated bundles (default: dist).",
)
parser.add_argument(
"--tag",
"--tags",
dest="tags",
help="Filter rules by tags (comma-separated, case-insensitive, AND logic). Example: --tag api,web-security",
)

cli_args = parser.parse_args()
source_paths = _resolve_source_paths(cli_args)
Expand Down Expand Up @@ -316,7 +350,16 @@ def _resolve_source_paths(args) -> list[Path]:
print()

# Convert all sources
aggregated = {"success": [], "errors": []}
aggregated = {"success": [], "errors": [], "skipped": []}
# Parse comma-separated tags and normalize to lowercase
filter_tags = None
if cli_args.tags:
filter_tags = [tag.strip().lower() for tag in cli_args.tags.split(",") if tag.strip()]

# Print tag filter info if active
if filter_tags:
print(f"Tag filter active: {', '.join(filter_tags)} (AND logic - rules must have all tags)\n")

for source_path in source_paths:
is_core = source_path == Path("sources/core")

Expand All @@ -325,11 +368,14 @@ def _resolve_source_paths(args) -> list[Path]:
str(source_path),
cli_args.output_dir,
include_claudecode=is_core,
version=version
version=version,
filter_tags=filter_tags
)

aggregated["success"].extend(results["success"])
aggregated["errors"].extend(results["errors"])
if "skipped" in results:
aggregated["skipped"].extend(results["skipped"])
print("")

if aggregated["errors"]:
Expand Down
14 changes: 12 additions & 2 deletions src/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from pathlib import Path

from language_mappings import languages_to_globs
from utils import parse_frontmatter_and_content
from utils import parse_frontmatter_and_content, validate_tags
from formats import (
BaseFormat,
ProcessedRule,
Expand Down Expand Up @@ -45,6 +45,7 @@ class ConversionResult:
basename: Filename without extension (e.g., 'my-rule')
outputs: Dictionary mapping format names to their outputs
languages: List of programming languages the rule applies to, empty list if always applies
tags: List of tags for categorizing and filtering rules
Example:
result = ConversionResult(
filename="my-rule.md",
Expand All @@ -56,14 +57,16 @@ class ConversionResult:
subpath=".cursor/rules"
)
},
languages=["python", "javascript"]
languages=["python", "javascript"],
tags=["authentication", "web-security"]
)
"""

filename: str
basename: str
outputs: dict[str, FormatOutput]
languages: list[str]
tags: list[str]


class RuleConverter:
Expand Down Expand Up @@ -159,6 +162,11 @@ def parse_rule(self, content: str, filename: str) -> ProcessedRule:
f"'languages' must be a non-empty list in {filename} when alwaysApply is false"
)

# Parse and validate tags (optional field)
tags = []
if "tags" in frontmatter:
tags = validate_tags(frontmatter["tags"], filename)

# Adding rule_id to the beginning of the content
rule_id = Path(filename).stem
markdown_content = f"rule_id: {rule_id}\n\n{markdown_content}"
Expand All @@ -169,6 +177,7 @@ def parse_rule(self, content: str, filename: str) -> ProcessedRule:
always_apply=always_apply,
content=markdown_content,
filename=filename,
tags=tags,
)

def generate_globs(self, languages: list[str]) -> str:
Expand Down Expand Up @@ -242,4 +251,5 @@ def convert(self, filepath: str) -> ConversionResult:
basename=basename,
outputs=outputs,
languages=rule.languages,
tags=rule.tags,
)
2 changes: 2 additions & 0 deletions src/formats/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ class ProcessedRule:
always_apply: Whether this rule should apply to all files
content: The actual rule content in markdown format
filename: Original filename of the rule
tags: List of tags for categorizing and filtering rules
"""

description: str
languages: list[str]
always_apply: bool
content: str
filename: str
tags: list[str]


class BaseFormat(ABC):
Expand Down
21 changes: 21 additions & 0 deletions src/tag_mappings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2025 Cisco Systems, Inc. and its affiliates
#
# SPDX-License-Identifier: Apache-2.0

"""
Tag Mappings

Centralized list of known tags for categorizing security rules.
"""

# Known tags used in rules
# Add new tags here as they are introduced in rules
KNOWN_TAGS = {
"authentication",
"data-security",
"infrastructure",
"privacy",
"secrets",
"web",
}

Loading
Loading