Converter Plugins
Converter plugins transform files before scanning to make them compatible with security scanners. For example, converting Jupyter notebooks to Python files.
For detailed visual diagrams of converter plugin architecture and workflow, see Converter Plugin Diagrams.
Converter Plugin Interface
Converter plugins must implement the ConverterPluginBase
interface:
from automated_security_helper.base.converter_plugin import ConverterPluginBase, ConverterPluginConfigBase
from automated_security_helper.plugins.decorators import ash_converter_plugin
@ash_converter_plugin
class MyConverter(ConverterPluginBase):
"""My custom converter implementation"""
def convert(self, target):
"""Convert the target file or directory"""
# Your code here
Converter Plugin Configuration
Define a configuration class for your converter:
from typing import List
from pydantic import Field
class MyConverterConfig(ConverterPluginConfigBase):
name: str = "my-converter"
enabled: bool = True
class Options:
file_extensions: List[str] = Field(default=[".ipynb"], description="File extensions to convert")
preserve_line_numbers: bool = Field(default=True, description="Preserve line numbers in converted files")
Converter Plugin Example
Here's a complete example of a custom converter plugin:
import json
import os
from pathlib import Path
from typing import List
from pydantic import Field
from automated_security_helper.base.converter_plugin import ConverterPluginBase, ConverterPluginConfigBase
from automated_security_helper.plugins.decorators import ash_converter_plugin
class JupyterConverterConfig(ConverterPluginConfigBase):
"""Configuration for JupyterConverter"""
name: str = "jupyter"
enabled: bool = True
class Options:
file_extensions: List[str] = Field(default=[".ipynb"], description="File extensions to convert")
preserve_line_numbers: bool = Field(default=True, description="Preserve line numbers in converted files")
include_markdown: bool = Field(default=False, description="Include markdown cells in output")
@ash_converter_plugin
class JupyterConverter(ConverterPluginBase):
"""Converts Jupyter notebooks to Python files"""
def convert(self, target: Path):
"""Convert Jupyter notebooks to Python files"""
if target.is_file():
if target.suffix in self.config.options.file_extensions:
return self._convert_file(target)
return None
# Process directory
converted_dir = self.converted_dir / target.name
converted_dir.mkdir(parents=True, exist_ok=True)
# Find all notebook files
notebook_files = []
for ext in self.config.options.file_extensions:
notebook_files.extend(target.glob(f"**/*{ext}"))
# Convert each notebook
for notebook_file in notebook_files:
rel_path = notebook_file.relative_to(target)
output_path = converted_dir / rel_path.with_suffix(".py")
output_path.parent.mkdir(parents=True, exist_ok=True)
try:
self._convert_notebook(notebook_file, output_path)
except Exception as e:
self._plugin_log(
f"Error converting {notebook_file}: {str(e)}",
level="ERROR",
append_to_stream="stderr",
)
return converted_dir
def _convert_file(self, file_path: Path):
"""Convert a single notebook file"""
output_path = self.converted_dir / file_path.with_suffix(".py").name
self.converted_dir.mkdir(parents=True, exist_ok=True)
try:
self._convert_notebook(file_path, output_path)
return output_path
except Exception as e:
self._plugin_log(
f"Error converting {file_path}: {str(e)}",
level="ERROR",
append_to_stream="stderr",
)
return None
def _convert_notebook(self, input_path: Path, output_path: Path):
"""Convert a notebook to a Python file"""
try:
with open(input_path, "r", encoding="utf-8") as f:
notebook = json.load(f)
with open(output_path, "w", encoding="utf-8") as f:
f.write(f"# Converted from {input_path.name}\n\n")
cell_count = 0
for cell in notebook.get("cells", []):
cell_type = cell.get("cell_type")
source = cell.get("source", [])
# Skip non-code cells if not including markdown
if cell_type != "code" and not self.config.options.include_markdown:
continue
# Join source lines
if isinstance(source, list):
source = "".join(source)
# Add cell marker
cell_count += 1
f.write(f"# Cell {cell_count} ({cell_type})\n")
# Write content
if cell_type == "code":
f.write(source)
else:
# Comment out markdown
for line in source.split("\n"):
f.write(f"# {line}\n")
f.write("\n\n")
return output_path
except Exception as e:
raise Exception(f"Failed to convert notebook: {str(e)}")
Converter Plugin Best Practices
- Preserve Line Numbers: Try to preserve line numbers for better mapping of findings
- Handle Directories: Support converting both individual files and directories
- Error Handling: Use try/except blocks to handle errors
- Logging: Use the
_plugin_log
method for logging - Return Paths: Return the path to the converted file or directory
Converter Plugin Configuration in ASH
Configure your converter in the ASH configuration file:
# .ash/.ash.yaml
converters:
jupyter:
enabled: true
options:
file_extensions: [".ipynb"]
preserve_line_numbers: true
include_markdown: false
Testing Converter Plugins
Create unit tests for your converter:
import pytest
from pathlib import Path
from automated_security_helper.base.plugin_context import PluginContext
from my_ash_plugins.converters import JupyterConverter
def test_jupyter_converter():
# Create a plugin context
context = PluginContext(
source_dir=Path("test_data"),
output_dir=Path("test_output"),
converted_dir=Path("test_output/converted")
)
# Create converter instance
converter = JupyterConverter(context=context)
# Create a test notebook
notebook_path = Path("test_data/test.ipynb")
with open(notebook_path, "w") as f:
f.write('{"cells": [{"cell_type": "code", "source": ["print(\\"Hello, world!\\")\\n"]}]}')
# Convert the notebook
converted_path = converter.convert(notebook_path)
# Assert conversion
assert converted_path is not None
assert converted_path.exists()
with open(converted_path, "r") as f:
content = f.read()
assert "print(\"Hello, world!\")" in content