Python CLI prototype for the thesis architecture. It demonstrates JSON workflow loading, adapter selection, Kali-based tool execution, evidence capture, normalised findings, traceability fields, and a readable XML report.
The prototype assumes a Kali Linux execution environment with the selected security tools installed. The CLI may run on another machine, but the tool adapters execute tools on Kali over SSH.
Set the environment first. PowerShell example:
$env:SECPLATFORM_KALI_HOST='<kali-host-or-ip>'
$env:SECPLATFORM_KALI_USER='<ssh-username>'
$env:SECPLATFORM_KALI_PASSWORD='<ssh-password>'
$env:SECPLATFORM_KALI_PORT='22'
$env:SECPLATFORM_PROJECT_PATH='.'
$env:SECPLATFORM_ZAP_TARGET_URL='http://127.0.0.1:8080'
$env:SECPLATFORM_REMOTE_TMP='/tmp'Bash example:
export SECPLATFORM_KALI_HOST='<kali-host-or-ip>'
export SECPLATFORM_KALI_USER='<ssh-username>'
export SECPLATFORM_KALI_PASSWORD='<ssh-password>'
export SECPLATFORM_KALI_PORT='22'
export SECPLATFORM_PROJECT_PATH='.'
export SECPLATFORM_ZAP_TARGET_URL='http://127.0.0.1:8080'
export SECPLATFORM_REMOTE_TMP='/tmp'Optional tool path overrides:
export SECPLATFORM_GITLEAKS_BIN='gitleaks'
export SECPLATFORM_TRIVY_BIN='trivy'
export SECPLATFORM_SEMGREP_BIN='semgrep'
export SECPLATFORM_ZAP_BIN='zaproxy'SECPLATFORM_KALI_PASSWORD is read at runtime by the SSH helper. It is not
stored in workflow JSON or reports. If you want to use a differently named
password variable, set SECPLATFORM_KALI_PASSWORD_ENV to that variable name.
Expected Kali tools:
- OpenSSH server reachable from the CLI machine.
python3andcurlfor running the included test applications.gitleaks,trivy,semgrep, andzaproxyfor the included workflows.
python sec_orchestrator.py validate example_workflow.json
python sec_orchestrator.py adapters
python sec_orchestrator.py normalizers
python sec_orchestrator.py run example_workflow.jsonShow command help:
python sec_orchestrator.py --help
python sec_orchestrator.py run --helpValidate a workflow without executing tools:
python sec_orchestrator.py validate example_workflow.json
python sec_orchestrator.py validate gitleaks_workflow.jsonList registered extension points:
python sec_orchestrator.py adapters
python sec_orchestrator.py normalizersRun the barebones no-tool workflow:
python sec_orchestrator.py run example_workflow.jsonRun a workflow and write artifacts to a custom directory:
python sec_orchestrator.py run example_workflow.json --output runs/dev-checkRun the Gitleaks self-scan through the Kali host:
python sec_orchestrator.py run gitleaks_workflow.jsonThe Gitleaks workflow copies the local project to a temporary directory on Kali,
runs gitleaks detect over SSH, stores the raw JSON output as evidence, and
normalises any findings into the common finding model. The SSH password is read
from the configured environment variable, so it is not stored in the workflow
JSON.
Run the Trivy self-scan through the Kali host:
python sec_orchestrator.py run trivy_workflow.jsonThe Trivy workflow copies the local project to Kali and runs trivy fs with JSON
output. It is currently configured for vulnerability, secret, and
misconfiguration scanning.
Run the Semgrep self-scan through the Kali host:
python sec_orchestrator.py run semgrep_workflow.jsonThe Semgrep workflow copies the local project to Kali and runs Semgrep with the
Python ruleset configured in semgrep_workflow.json.
Prepare an OWASP ZAP web scan:
python sec_orchestrator.py validate zap_workflow.jsonBefore running ZAP, edit zap_workflow.json and set target_url to a web
application that you own or have explicit permission to test, or set
SECPLATFORM_ZAP_TARGET_URL. Then run:
python sec_orchestrator.py run zap_workflow.jsonRun the realistic NIST CSF 2.0 application security workflow:
python sec_orchestrator.py validate nist_csf_2_0_full_workflow.json
python sec_orchestrator.py validate nist_csf_2_0_code_workflow.jsonUse the full workflow when there is a running web application that ZAP is
allowed to scan. Set SECPLATFORM_ZAP_TARGET_URL, then run:
python sec_orchestrator.py run nist_csf_2_0_full_workflow.jsonUse the code-only workflow when there is no website or web service available:
python sec_orchestrator.py run nist_csf_2_0_code_workflow.jsonThere is no meaningful automatic fallback for OWASP ZAP when no website exists. ZAP is a dynamic application security testing tool, so it needs a reachable HTTP or HTTPS target. In that situation, the correct fallback is to use the workflow without ZAP and rely on the source, dependency, secret, and configuration checks.
The NIST CSF 2.0 workflows use high-level CSF outcomes as requirement IDs and
map them to concrete security capabilities. NIST CSF 2.0 is an outcome-oriented
framework, so these mappings are pragmatic prototype mappings rather than
official tool prescriptions. Reference:
https://nvlpubs.nist.gov/nistpubs/CSWP/NIST.CSWP.29.pdf.
The repository includes two small Python web applications for repeatable testing:
test_apps/vulnerable/app.py: intentionally vulnerable target with reflected XSS, SQL injection-style query construction, weak cookie handling, missing security headers, and a fake committed secret.test_apps/fixed/app.py: corrected copy with output escaping, parameterized SQL, safer cookie flags, security headers, and no fake secret.
The ZAP workflows expect the app to run on the Kali host because the default
ZAP target is http://127.0.0.1:8080 from Kali's point of view.
Start the vulnerable app on Kali:
$remote = "$env:SECPLATFORM_KALI_USER@$env:SECPLATFORM_KALI_HOST"
$testApp = "$env:SECPLATFORM_REMOTE_TMP/secplatform-test-app.py"
scp test_apps/vulnerable/app.py "${remote}:$testApp"
ssh $remote "pkill -f secplatform-test-app.py || true; nohup python3 $testApp --host 127.0.0.1 --port 8080 > $env:SECPLATFORM_REMOTE_TMP/secplatform-test-app.log 2>&1 &"
ssh $remote "curl -s http://127.0.0.1:8080/health"Then run the full vulnerable-app workflow:
python sec_orchestrator.py run nist_csf_2_0_vulnerable_test_workflow.jsonStart the fixed app on Kali:
$remote = "$env:SECPLATFORM_KALI_USER@$env:SECPLATFORM_KALI_HOST"
$testApp = "$env:SECPLATFORM_REMOTE_TMP/secplatform-test-app.py"
scp test_apps/fixed/app.py "${remote}:$testApp"
ssh $remote "pkill -f secplatform-test-app.py || true; nohup python3 $testApp --host 127.0.0.1 --port 8080 > $env:SECPLATFORM_REMOTE_TMP/secplatform-test-app.log 2>&1 &"
ssh $remote "curl -s http://127.0.0.1:8080/health"Then run the full fixed-app workflow:
python sec_orchestrator.py run nist_csf_2_0_fixed_test_workflow.jsonThe vulnerable workflow is expected to produce findings from all four tools:
Gitleaks, Semgrep, Trivy, and OWASP ZAP. The fixed workflow is expected to clear
the source, secret, and filesystem checks. ZAP may still report
Authentication Request Identified as an informational item because the fixed
app still contains a login form; that is useful metadata rather than a
vulnerability.
The test workflows configure Semgrep with a project-local rule file by setting
config_from_project to true. That lets a workflow use rules bundled with the
scanned project after it has been copied to the Kali host.
Stop the test app on Kali:
$remote = "$env:SECPLATFORM_KALI_USER@$env:SECPLATFORM_KALI_HOST"
ssh $remote "pkill -f secplatform-test-app.py || true"Each run creates a directory under runs/ containing:
workflow.snapshot.json: the workflow definition used for the run.run.json: structured execution metadata and normalised findings.evidence/*.json: raw output and metadata for each workflow step.report.xml: human-readable XML report.
The files are split to match the architecture diagram while keeping the prototype small:
sec_orchestrator.py: CLI entry point for the interface layer.secplatform/cli.py: interface layer command handling.secplatform/repositories.py: requirements/workflow repository plus evidence storage.secplatform/orchestrator.py: core workflow orchestrator.secplatform/adapters/: tool adapter layer.secplatform/adapters/base.py: common adapter interface.secplatform/adapters/registry.py: adapter map and selection logic.secplatform/adapters/noop.py: current no-op adapter.secplatform/normalizers/: result normalisation layer.secplatform/normalizers/base.py: common normalizer interface.secplatform/normalizers/registry.py: normalizer map and selection logic.secplatform/normalizers/default.py: fallback normalizer for common finding dictionaries.secplatform/reporting.py: XML report generator.secplatform/models.py: shared data contracts used between layers.requirements/: reusable requirement and control-set JSON files.test_apps/: vulnerable and fixed demo applications for workflow testing.
sec_orchestrator.py is only a start script. It intentionally has no workflow,
adapter, repository, or reporting logic. The actual CLI behavior lives in
secplatform/cli.py.
Workflows are JSON documents. The project section records the local path or
GitHub/Git URL that the workflow applies to.
{
"id": "example",
"name": "Example Workflow",
"version": "0.1.0",
"project": {
"source": "github",
"url": "https://github.com/example/project",
"branch": "main"
},
"requirements": [],
"steps": []
}Workflow JSON may reference environment variables with ${NAME}. If the
variable is required and missing, validation fails before execution. Use
${NAME:default} when a safe default is acceptable.
{
"project": {
"source": "local",
"path": "${SECPLATFORM_PROJECT_PATH:.}"
},
"steps": [
{
"parameters": {
"host": "${SECPLATFORM_KALI_HOST}",
"username": "${SECPLATFORM_KALI_USER}",
"password_env_var": "${SECPLATFORM_KALI_PASSWORD_ENV:SECPLATFORM_KALI_PASSWORD}"
}
}
]
}The current built-in adapter is noop, which supports the placeholder and
manual capabilities without invoking external tools. Real Kali tool adapters
can be added by implementing the same adapter interface and registering the
adapter in the adapter registry map.
validate checks the JSON structure and requirement mappings. run also checks
that a requested adapter exists and supports the requested capability.
The main rule is that requirements, workflow changes, and tool changes should live in different places:
- Requirements and mappings belong in the workflow JSON.
- Step parameters and thresholds belong in the workflow JSON.
- Tool command details belong inside adapters.
- Tool-specific parsing belongs inside normalizers.
- Reports should consume the normalised run result, not raw tool output.
This keeps the workflow orchestrator independent from specific security tools.
Add a requirement object to the requirements array. Requirements describe the
security intent, not the tool command.
{
"requirements": [
{
"id": "REQ-SECRETS-001",
"title": "Committed secrets must be detected",
"description": "The project should be checked for hard-coded secrets before release.",
"source": "Internal secure development policy"
}
]
}Then link a workflow step to that requirement with requirement_ids.
{
"steps": [
{
"id": "review-secret-handling",
"name": "Review secret handling",
"capability": "manual",
"adapter": "noop",
"requirement_ids": ["REQ-SECRETS-001"],
"parameters": {
"message": "Placeholder until a real secret detection adapter is added."
}
}
]
}That is the S1 path: a new requirement can be added and mapped without changing the orchestrator.
For small examples, requirements can stay inline in the workflow JSON. For organisational policies or larger control sets, put them in a separate JSON file and reference it from the workflow:
{
"requirements_file": "requirements/internal_policy.json"
}The external file may either be a plain list:
[
{
"id": "REQ-SECRETS-001",
"title": "Committed secrets must be detected",
"description": "The project should be checked for hard-coded secrets before release.",
"source": "Internal secure development policy"
}
]Or an object with a requirements list:
{
"requirements": [
{
"id": "REQ-SECRETS-001",
"title": "Committed secrets must be detected"
}
]
}The repository layer merges external requirements with any inline requirements before validation. The workflow orchestrator only sees the structured workflow definition.
Change step behavior through parameters, not Python orchestration code.
{
"id": "dependency-scan",
"name": "Scan dependencies",
"capability": "manual",
"adapter": "noop",
"parameters": {
"severity_threshold": "high",
"include_dev_dependencies": false
}
}Later, a real adapter can decide how those parameters map to a concrete command. That is the S2 path: thresholds and configuration live in the workflow definition.
Steps can include mitigation guidance. When an adapter returns findings for that step, the report can link the finding to a plausible response.
{
"id": "review-secret-handling",
"name": "Review secret handling",
"capability": "manual",
"adapter": "noop",
"mitigation": {
"title": "Rotate exposed credentials",
"guidance": "Revoke the exposed credential, rotate it, and remove it from repository history.",
"references": [
"https://docs.github.com/en/code-security/secret-scanning"
]
}
}That supports S6 and S7: findings remain traceable and actionable.
Adapters are the only place where concrete tool execution details should live.
To add a real tool, create a new file in secplatform/adapters/. For example,
secplatform/adapters/example_tool.py.
id: the adapter name used in workflow JSON.capabilities: the abstract capabilities the adapter can satisfy.execute(step, execution_context): the method that invokes the tool and returns anAdapterResult.
Minimal shape:
from secplatform.models import AdapterResult, ExecutionContext, WorkflowStep
class ExampleAdapter:
id = "example-tool"
capabilities = ("secret-detection",)
def execute(self, step: WorkflowStep, execution_context: ExecutionContext) -> AdapterResult:
return AdapterResult(
status="passed",
exit_code=0,
raw_output={
"tool": self.id,
"message": "Replace this with captured tool output."
},
findings=[]
)Then register it in the adapter map in secplatform/adapters/registry.py:
from .example_tool import ExampleAdapter
ADAPTER_CLASSES = {
NoOpAdapter.id: NoOpAdapter,
ExampleAdapter.id: ExampleAdapter,
}After that, a workflow step can request it explicitly:
{
"id": "detect-committed-secrets",
"name": "Detect committed secrets",
"capability": "secret-detection",
"adapter": "example-tool",
"parameters": {}
}That is the S3 path: adding a tool should require a new adapter and registration, not changes to the workflow orchestrator.
For simple adapters, the adapter may return common finding dictionaries directly
in AdapterResult.findings. The default normalizer converts those dictionaries
to the platform finding model.
return AdapterResult(
status="completed_with_findings",
exit_code=1,
raw_output={"tool_output": "..."},
findings=[
{
"id": "SECRET-001",
"title": "Possible committed secret",
"severity": "high",
"description": "A token-like value was detected in the repository."
}
]
)The raw tool output still goes into evidence/*.json; the report uses the
normalised finding fields.
Real tools often produce their own JSON, XML, or text output. In that case, keep the adapter focused on execution and place parsing logic in a normalizer.
Create a file in secplatform/normalizers/, for example
secplatform/normalizers/example_tool.py.
from secplatform.models import AdapterResult, Finding, ToolExecution, WorkflowStep
class ExampleToolNormalizer:
id = "example-tool"
adapter_ids = ("example-tool",)
capabilities = ()
def normalise(
self,
step: WorkflowStep,
execution: ToolExecution,
adapter_result: AdapterResult,
) -> list[Finding]:
return [
Finding(
id=f"{execution.id}-finding-1",
title="Parsed finding title",
severity="high",
description="Parsed from the tool-specific raw output.",
requirement_ids=step.requirement_ids,
step_id=step.id,
execution_id=execution.id,
evidence_file=execution.evidence_file,
mitigation=step.mitigation,
)
]Then register it in secplatform/normalizers/registry.py:
from .example_tool import ExampleToolNormalizer
NORMALIZER_CLASSES = {
DefaultFindingNormalizer.id: DefaultFindingNormalizer,
ExampleToolNormalizer.id: ExampleToolNormalizer,
}Normalizers are selected by adapter ID first, then by capability, then by the default normalizer.
The prototype intentionally has one report output: developer-readable XML. The
report generator lives in secplatform/reporting.py.
Change that file when the report needs different sections, names, ordering, or
traceability details. The report generator should consume the normalised
RunResult and WorkflowDefinition; it should not parse raw tool output.
Workflow steps can be written in terms of capability rather than a concrete tool. For example:
{
"id": "detect-committed-secrets",
"name": "Detect committed secrets",
"capability": "secret-detection",
"parameters": {}
}If adapter is omitted, the registry selects the first adapter that supports the
requested capability. To replace the tool, add/register a different adapter for
the same capability or change the explicit adapter field in JSON.
That is the S4 path: the workflow expresses what should happen, while the adapter decides how it happens.
The workflow stores the project target so repeated runs can be inspected later.
Local project:
{
"project": {
"source": "local",
"path": "/home/kali/example-project"
}
}GitHub or Git project:
{
"project": {
"source": "github",
"url": "https://github.com/example/project",
"branch": "main"
}
}The current prototype records this target. A later adapter can use it to clone, checkout, or scan the project.
- S1: requirements are structured JSON objects and steps can link to them by ID.
- S2: step parameters live in the workflow definition, not orchestration code.
- S3: adapters are isolated behind a stable
ToolAdapterinterface. - S4: steps express capabilities; adapter choice can be explicit or selected by capability.
- S5: each execution stores workflow snapshots, metadata, evidence, and reports.
- S6: findings include requirement, step, execution, and evidence references.
- S7: findings can carry mitigation guidance and references for user-facing reports.