safeworkflow 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- safeworkflow/__init__.py +18 -0
- safeworkflow/cli.py +103 -0
- safeworkflow/config.py +33 -0
- safeworkflow/patterns.py +110 -0
- safeworkflow/sanitizer.py +57 -0
- safeworkflow/scanner.py +98 -0
- safeworkflow/scorer.py +57 -0
- safeworkflow/types.py +37 -0
- safeworkflow-1.0.0.dist-info/METADATA +105 -0
- safeworkflow-1.0.0.dist-info/RECORD +12 -0
- safeworkflow-1.0.0.dist-info/WHEEL +4 -0
- safeworkflow-1.0.0.dist-info/entry_points.txt +2 -0
safeworkflow/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""safeworkflow - Prompt injection and supply-chain risk protection."""
|
|
2
|
+
|
|
3
|
+
from .config import Settings
|
|
4
|
+
from .sanitizer import sanitize
|
|
5
|
+
from .scanner import scan
|
|
6
|
+
from .scorer import RiskLevel, Score
|
|
7
|
+
from .types import ScanIssue, ScanResult
|
|
8
|
+
|
|
9
|
+
__version__ = "1.0.0"
|
|
10
|
+
__all__ = [
|
|
11
|
+
"scan",
|
|
12
|
+
"Score",
|
|
13
|
+
"RiskLevel",
|
|
14
|
+
"sanitize",
|
|
15
|
+
"Settings",
|
|
16
|
+
"ScanResult",
|
|
17
|
+
"ScanIssue",
|
|
18
|
+
]
|
safeworkflow/cli.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""CLI for safeworkflow."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich import print as rprint
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from .sanitizer import sanitize
|
|
12
|
+
from .scanner import scan, scan_file
|
|
13
|
+
from .types import ScanResult
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="Prompt injection and supply-chain risk protection")
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command()
|
|
20
|
+
def scan_cmd(
|
|
21
|
+
source: str = typer.Argument(..., help="File or text to scan"),
|
|
22
|
+
fail_on: str = typer.Option("high", "--fail-on", "-f", help="Fail on risk level"),
|
|
23
|
+
format: str = typer.Option("text", "--format", help="Output format: text, json"),
|
|
24
|
+
max_score: int = typer.Option(100, "--max-score", help="Maximum risk score"),
|
|
25
|
+
) -> int:
|
|
26
|
+
"""Scan content or file for security risks."""
|
|
27
|
+
path = Path(source)
|
|
28
|
+
|
|
29
|
+
if path.exists():
|
|
30
|
+
result = scan_file(str(path), fail_on=fail_on)
|
|
31
|
+
else:
|
|
32
|
+
result = scan(source, fail_on=fail_on, max_score=max_score)
|
|
33
|
+
|
|
34
|
+
if format == "json":
|
|
35
|
+
output = {
|
|
36
|
+
"score": result.score,
|
|
37
|
+
"risk_level": result.risk_level.value,
|
|
38
|
+
"is_safe": result.is_safe,
|
|
39
|
+
"issue_count": len(result.issues),
|
|
40
|
+
"issues": [
|
|
41
|
+
{
|
|
42
|
+
"line": i.line,
|
|
43
|
+
"column": i.column,
|
|
44
|
+
"message": i.message,
|
|
45
|
+
"risk_level": i.risk_level.value,
|
|
46
|
+
"pattern": i.pattern_name,
|
|
47
|
+
}
|
|
48
|
+
for i in result.issues
|
|
49
|
+
],
|
|
50
|
+
}
|
|
51
|
+
print(json.dumps(output, indent=2))
|
|
52
|
+
else:
|
|
53
|
+
_print_result(result)
|
|
54
|
+
|
|
55
|
+
return 1 if not result.is_safe else 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.command("sanitize")
|
|
59
|
+
def sanitize_cmd(
|
|
60
|
+
source: str = typer.Argument(..., help="File or text to sanitize"),
|
|
61
|
+
output: str | None = typer.Option(None, "--output", "-o", help="Output file"),
|
|
62
|
+
replacement: str = typer.Option(
|
|
63
|
+
"[REDACTED]", "--replacement", "-r", help="Replacement text"
|
|
64
|
+
),
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Sanitize content by removing security risks."""
|
|
67
|
+
path = Path(source)
|
|
68
|
+
content = path.read_text(encoding="utf-8") if path.exists() else source
|
|
69
|
+
result = sanitize(content, replacement=replacement)
|
|
70
|
+
|
|
71
|
+
if output:
|
|
72
|
+
Path(output).write_text(result, encoding="utf-8")
|
|
73
|
+
rprint(f"[green]Sanitized output written to {output}[/green]")
|
|
74
|
+
else:
|
|
75
|
+
print(result)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _print_result(result: ScanResult) -> None:
|
|
79
|
+
"""Print scan result in human-readable format."""
|
|
80
|
+
rprint(f"\n[bold]Risk Score:[/bold] {result.score}/100")
|
|
81
|
+
rprint(f"[bold]Risk Level:[/bold] {result.risk_level.value.upper()}")
|
|
82
|
+
status = "[green]SAFE[/green]" if result.is_safe else "[red]UNSAFE[/red]"
|
|
83
|
+
rprint(f"[bold]Status:[/bold] {status}")
|
|
84
|
+
|
|
85
|
+
if result.issues:
|
|
86
|
+
table = Table(title="Detected Issues")
|
|
87
|
+
table.add_column("Line", style="cyan")
|
|
88
|
+
table.add_column("Pattern", style="magenta")
|
|
89
|
+
table.add_column("Message", style="yellow")
|
|
90
|
+
table.add_column("Risk", style="red")
|
|
91
|
+
|
|
92
|
+
for issue in result.issues:
|
|
93
|
+
table.add_row(
|
|
94
|
+
str(issue.line),
|
|
95
|
+
issue.pattern_name,
|
|
96
|
+
issue.message[:50],
|
|
97
|
+
issue.risk_level.value.upper(),
|
|
98
|
+
)
|
|
99
|
+
rprint(table)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
app()
|
safeworkflow/config.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Configuration for safeworkflow."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
from pydantic_settings import BaseSettings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Settings(BaseSettings):
|
|
9
|
+
"""Configuration settings for safeworkflow."""
|
|
10
|
+
|
|
11
|
+
fail_on: str = Field(default="high", description="Minimum risk level to fail")
|
|
12
|
+
max_risk_score: int = Field(default=70, description="Maximum acceptable risk score")
|
|
13
|
+
enable_ai_patterns: bool = Field(
|
|
14
|
+
default=True, description="Enable AI-specific patterns"
|
|
15
|
+
)
|
|
16
|
+
enable_supply_chain: bool = Field(
|
|
17
|
+
default=True, description="Enable supply-chain detection"
|
|
18
|
+
)
|
|
19
|
+
custom_patterns: list[str] = Field(
|
|
20
|
+
default_factory=list, description="Custom regex patterns"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
model_config = {"env_prefix": "SAFEWORKFLOW_", "env_file": ".env"}
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def should_fail_on(self) -> dict[str, int]:
|
|
27
|
+
"""Map risk level to minimum score for fail."""
|
|
28
|
+
return {
|
|
29
|
+
"low": 25,
|
|
30
|
+
"medium": 50,
|
|
31
|
+
"high": 75,
|
|
32
|
+
"critical": 90,
|
|
33
|
+
}
|
safeworkflow/patterns.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Pattern database for detecting injection and supply-chain risks."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import NamedTuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Pattern(NamedTuple):
|
|
8
|
+
"""A detection pattern."""
|
|
9
|
+
name: str
|
|
10
|
+
pattern: re.Pattern
|
|
11
|
+
risk_level: str
|
|
12
|
+
description: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Base injection patterns
|
|
16
|
+
INJECTION_PATTERNS = [
|
|
17
|
+
Pattern(
|
|
18
|
+
name="ignore_previous",
|
|
19
|
+
pattern=re.compile(
|
|
20
|
+
r"ignore\s+(all\s+)?(previous|above|prior|earlier)",
|
|
21
|
+
re.IGNORECASE
|
|
22
|
+
),
|
|
23
|
+
risk_level="critical",
|
|
24
|
+
description="Attempts to ignore previous instructions",
|
|
25
|
+
),
|
|
26
|
+
Pattern(
|
|
27
|
+
name="system_override",
|
|
28
|
+
pattern=re.compile(
|
|
29
|
+
r"(you are now|new instructions|override|disregard).*system",
|
|
30
|
+
re.IGNORECASE
|
|
31
|
+
),
|
|
32
|
+
risk_level="critical",
|
|
33
|
+
description="System instruction override attempt",
|
|
34
|
+
),
|
|
35
|
+
Pattern(
|
|
36
|
+
name="jailbreak",
|
|
37
|
+
pattern=re.compile(
|
|
38
|
+
r"(jailbreak|dan\s*mode|developer\s*mode|unfiltered)",
|
|
39
|
+
re.IGNORECASE
|
|
40
|
+
),
|
|
41
|
+
risk_level="critical",
|
|
42
|
+
description="Jailbreak or DAN mode attempt",
|
|
43
|
+
),
|
|
44
|
+
Pattern(
|
|
45
|
+
name="role_injection",
|
|
46
|
+
pattern=re.compile(
|
|
47
|
+
r"(you are|act as|pretend to be|roleplay).*?(assistant|admin|root)",
|
|
48
|
+
re.IGNORECASE
|
|
49
|
+
),
|
|
50
|
+
risk_level="high",
|
|
51
|
+
description="Role injection attempt",
|
|
52
|
+
),
|
|
53
|
+
Pattern(
|
|
54
|
+
name="command_injection",
|
|
55
|
+
pattern=re.compile(
|
|
56
|
+
r"(rm\s+-rf|sudo|chmod|curl\s+\||\|\s*bash|\$\(.*\)|`.*?`)",
|
|
57
|
+
re.IGNORECASE
|
|
58
|
+
),
|
|
59
|
+
risk_level="high",
|
|
60
|
+
description="Shell command injection attempt",
|
|
61
|
+
),
|
|
62
|
+
Pattern(
|
|
63
|
+
name="javascript_protocol",
|
|
64
|
+
pattern=re.compile(
|
|
65
|
+
r"javascript:|data:text/html",
|
|
66
|
+
re.IGNORECASE
|
|
67
|
+
),
|
|
68
|
+
risk_level="medium",
|
|
69
|
+
description="JavaScript protocol in URL",
|
|
70
|
+
),
|
|
71
|
+
Pattern(
|
|
72
|
+
name="supply_chain_pkg",
|
|
73
|
+
pattern=re.compile(
|
|
74
|
+
r"(pip\s+install|npm\s+install|go\s+get).*-[a-z0-9]{8,12}",
|
|
75
|
+
re.IGNORECASE
|
|
76
|
+
),
|
|
77
|
+
risk_level="high",
|
|
78
|
+
description="Suspicious package name with random suffix",
|
|
79
|
+
),
|
|
80
|
+
Pattern(
|
|
81
|
+
name="typosquatting",
|
|
82
|
+
pattern=re.compile(
|
|
83
|
+
r"(requessts|requsts|resquests|numpyy|pandas1)",
|
|
84
|
+
re.IGNORECASE
|
|
85
|
+
),
|
|
86
|
+
risk_level="high",
|
|
87
|
+
description="Typosquatting attempt",
|
|
88
|
+
),
|
|
89
|
+
Pattern(
|
|
90
|
+
name="env_leak",
|
|
91
|
+
pattern=re.compile(
|
|
92
|
+
r"(OPENAI_API_KEY|ANTHROPIC_API|SECRET|TOKEN).{0,20}(['\"]?\w{20,})",
|
|
93
|
+
re.IGNORECASE
|
|
94
|
+
),
|
|
95
|
+
risk_level="medium",
|
|
96
|
+
description="Potential credential leak",
|
|
97
|
+
),
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_patterns(enable_supply_chain: bool = True) -> list[Pattern]:
|
|
102
|
+
"""Get all detection patterns based on configuration."""
|
|
103
|
+
patterns = list(INJECTION_PATTERNS)
|
|
104
|
+
if not enable_supply_chain:
|
|
105
|
+
patterns = [
|
|
106
|
+
p
|
|
107
|
+
for p in patterns
|
|
108
|
+
if "supply" not in p.name.lower() and "typo" not in p.name.lower()
|
|
109
|
+
]
|
|
110
|
+
return patterns
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Content sanitizer for removing sensitive/injection patterns."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from .patterns import get_patterns
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def sanitize(
|
|
8
|
+
content: str,
|
|
9
|
+
*,
|
|
10
|
+
replacement: str = "[REDACTED]",
|
|
11
|
+
enable_supply_chain: bool = True,
|
|
12
|
+
) -> str:
|
|
13
|
+
"""Sanitize content by removing/redacting security risks.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
content: Text to sanitize.
|
|
17
|
+
replacement: Text to replace detected patterns with.
|
|
18
|
+
enable_supply_chain: Whether to check supply-chain patterns.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Sanitized content.
|
|
22
|
+
"""
|
|
23
|
+
patterns = get_patterns(enable_supply_chain=enable_supply_chain)
|
|
24
|
+
result = content
|
|
25
|
+
|
|
26
|
+
for pattern in patterns:
|
|
27
|
+
result = pattern.pattern.sub(replacement, result)
|
|
28
|
+
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def sanitize_file(
|
|
33
|
+
input_path: str,
|
|
34
|
+
output_path: str | None = None,
|
|
35
|
+
*,
|
|
36
|
+
replacement: str = "[REDACTED]",
|
|
37
|
+
) -> str:
|
|
38
|
+
"""Sanitize a file and optionally write to output.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
input_path: Path to input file.
|
|
42
|
+
output_path: Optional path for sanitized output.
|
|
43
|
+
replacement: Text to replace detected patterns with.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Sanitized content.
|
|
47
|
+
"""
|
|
48
|
+
with open(input_path, encoding="utf-8") as f:
|
|
49
|
+
content = f.read()
|
|
50
|
+
|
|
51
|
+
result = sanitize(content, replacement=replacement)
|
|
52
|
+
|
|
53
|
+
if output_path:
|
|
54
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
55
|
+
f.write(result)
|
|
56
|
+
|
|
57
|
+
return result
|
safeworkflow/scanner.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Content scanner for detecting security risks."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from .patterns import get_patterns
|
|
5
|
+
from .scorer import Score
|
|
6
|
+
from .types import RiskLevel, ScanIssue, ScanResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def scan(
|
|
10
|
+
content: str,
|
|
11
|
+
*,
|
|
12
|
+
fail_on: str = "high",
|
|
13
|
+
enable_supply_chain: bool = True,
|
|
14
|
+
max_score: int = 100,
|
|
15
|
+
) -> ScanResult:
|
|
16
|
+
"""Scan content for injection and supply-chain risks.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
content: Text to scan for security issues.
|
|
20
|
+
fail_on: Minimum risk level that triggers failure.
|
|
21
|
+
enable_supply_chain: Whether to check supply-chain patterns.
|
|
22
|
+
max_score: Maximum possible risk score.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
ScanResult with issues and risk assessment.
|
|
26
|
+
"""
|
|
27
|
+
issues: list[ScanIssue] = []
|
|
28
|
+
patterns = get_patterns(enable_supply_chain=enable_supply_chain)
|
|
29
|
+
|
|
30
|
+
lines = content.split("\n")
|
|
31
|
+
for line_num, line in enumerate(lines, 1):
|
|
32
|
+
for pattern in patterns:
|
|
33
|
+
for match in pattern.pattern.finditer(line):
|
|
34
|
+
issue = ScanIssue(
|
|
35
|
+
line=line_num,
|
|
36
|
+
column=match.start() + 1,
|
|
37
|
+
message=f"{pattern.description}: '{match.group()}'",
|
|
38
|
+
risk_level=RiskLevel(pattern.risk_level),
|
|
39
|
+
pattern_name=pattern.name,
|
|
40
|
+
suggestion=_get_suggestion(pattern.name),
|
|
41
|
+
)
|
|
42
|
+
issues.append(issue)
|
|
43
|
+
|
|
44
|
+
score = Score.calculate(issues, max_score=max_score)
|
|
45
|
+
risk_level = _determine_risk_level(score)
|
|
46
|
+
threshold = Score.threshold_for(fail_on)
|
|
47
|
+
is_safe = score < threshold
|
|
48
|
+
|
|
49
|
+
return ScanResult(
|
|
50
|
+
content=content,
|
|
51
|
+
issues=issues,
|
|
52
|
+
score=score,
|
|
53
|
+
risk_level=risk_level,
|
|
54
|
+
is_safe=is_safe,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def scan_file(
|
|
59
|
+
path: str,
|
|
60
|
+
*,
|
|
61
|
+
fail_on: str = "high",
|
|
62
|
+
encoding: str = "utf-8",
|
|
63
|
+
) -> ScanResult:
|
|
64
|
+
"""Scan a file for security risks.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
path: Path to file to scan.
|
|
68
|
+
fail_on: Minimum risk level that triggers failure.
|
|
69
|
+
encoding: File encoding.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
ScanResult with issues and risk assessment.
|
|
73
|
+
"""
|
|
74
|
+
with open(path, encoding=encoding) as f:
|
|
75
|
+
content = f.read()
|
|
76
|
+
return scan(content, fail_on=fail_on)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _get_suggestion(pattern_name: str) -> str | None:
|
|
80
|
+
"""Get remediation suggestion for a pattern."""
|
|
81
|
+
suggestions = {
|
|
82
|
+
"ignore_previous": "Remove instruction override attempts",
|
|
83
|
+
"system_override": "Avoid system instruction manipulation",
|
|
84
|
+
"jailbreak": "Block jailbreak patterns entirely",
|
|
85
|
+
"role_injection": "Sanitize role-playing attempts",
|
|
86
|
+
}
|
|
87
|
+
return suggestions.get(pattern_name)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _determine_risk_level(score: int) -> RiskLevel:
|
|
91
|
+
"""Determine risk level from score."""
|
|
92
|
+
if score >= 90:
|
|
93
|
+
return RiskLevel.CRITICAL
|
|
94
|
+
elif score >= 70:
|
|
95
|
+
return RiskLevel.HIGH
|
|
96
|
+
elif score >= 40:
|
|
97
|
+
return RiskLevel.MEDIUM
|
|
98
|
+
return RiskLevel.LOW
|
safeworkflow/scorer.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Risk scoring engine for safeworkflow."""
|
|
2
|
+
|
|
3
|
+
from .types import RiskLevel, ScanIssue
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Score:
|
|
7
|
+
"""Risk scoring utilities."""
|
|
8
|
+
|
|
9
|
+
WEIGHTS = {
|
|
10
|
+
RiskLevel.LOW: 1,
|
|
11
|
+
RiskLevel.MEDIUM: 3,
|
|
12
|
+
RiskLevel.HIGH: 7,
|
|
13
|
+
RiskLevel.CRITICAL: 15,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def calculate(issues: list[ScanIssue], max_score: int = 100) -> int:
|
|
18
|
+
"""Calculate risk score from issues.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
issues: List of detected security issues.
|
|
22
|
+
max_score: Maximum possible score.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Risk score 0-100.
|
|
26
|
+
"""
|
|
27
|
+
if not issues:
|
|
28
|
+
return 0
|
|
29
|
+
|
|
30
|
+
# Higher weighting for critical issues
|
|
31
|
+
weights = {
|
|
32
|
+
RiskLevel.CRITICAL: 40,
|
|
33
|
+
RiskLevel.HIGH: 25,
|
|
34
|
+
RiskLevel.MEDIUM: 10,
|
|
35
|
+
RiskLevel.LOW: 5,
|
|
36
|
+
}
|
|
37
|
+
total = sum(weights.get(issue.risk_level, 5) for issue in issues)
|
|
38
|
+
# Cap at max_score
|
|
39
|
+
return min(total, max_score)
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def threshold_for(level: str) -> int:
|
|
43
|
+
"""Get score threshold for a risk level.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
level: Risk level string (low/medium/high/critical).
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Score threshold.
|
|
50
|
+
"""
|
|
51
|
+
thresholds = {
|
|
52
|
+
"low": 25,
|
|
53
|
+
"medium": 50,
|
|
54
|
+
"high": 75,
|
|
55
|
+
"critical": 90,
|
|
56
|
+
}
|
|
57
|
+
return thresholds.get(level.lower(), 75)
|
safeworkflow/types.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Core types for safeworkflow."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RiskLevel(str, Enum):
|
|
8
|
+
"""Risk severity levels."""
|
|
9
|
+
LOW = "low"
|
|
10
|
+
MEDIUM = "medium"
|
|
11
|
+
HIGH = "high"
|
|
12
|
+
CRITICAL = "critical"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ScanIssue:
|
|
17
|
+
"""Represents a detected security issue."""
|
|
18
|
+
line: int
|
|
19
|
+
column: int
|
|
20
|
+
message: str
|
|
21
|
+
risk_level: RiskLevel
|
|
22
|
+
pattern_name: str
|
|
23
|
+
suggestion: str | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ScanResult:
|
|
28
|
+
"""Result of scanning content for security issues."""
|
|
29
|
+
content: str
|
|
30
|
+
issues: list[ScanIssue]
|
|
31
|
+
score: int
|
|
32
|
+
risk_level: RiskLevel
|
|
33
|
+
is_safe: bool
|
|
34
|
+
|
|
35
|
+
def __bool__(self) -> bool:
|
|
36
|
+
"""Return True if content is safe."""
|
|
37
|
+
return self.is_safe
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: safeworkflow
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Prompt injection and supply-chain risk protection for agentic workflows
|
|
5
|
+
Project-URL: Homepage, https://github.com/maheshmakvana/safeworkflow
|
|
6
|
+
Project-URL: Documentation, https://github.com/maheshmakvana/safeworkflow#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/maheshmakvana/safeworkflow
|
|
8
|
+
Project-URL: Issues, https://github.com/maheshmakwana/safeworkflow/issues
|
|
9
|
+
Author-email: Mahesh Makwana <mahesh.makwana787@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: agentic-workflows,ai-safety,llm-security,prompt-injection,security,supply-chain
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
23
|
+
Requires-Dist: pydantic>=2.0.0
|
|
24
|
+
Requires-Dist: rich>=13.0.0
|
|
25
|
+
Requires-Dist: typer>=0.9.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: build>=1.0.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: twine>=5.0.0; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# SafeWorkflow
|
|
36
|
+
|
|
37
|
+
**Prompt injection and supply-chain risk protection for agentic workflows**
|
|
38
|
+
|
|
39
|
+
[](https://badge.fury.io/py/safeworkflow)
|
|
40
|
+
[](https://www.python.org/downloads/)
|
|
41
|
+
[](https://opensource.org/licenses/MIT)
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install safeworkflow
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
### Python API
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from safeworkflow import scan, sanitize
|
|
55
|
+
|
|
56
|
+
# Scan for injection risks
|
|
57
|
+
result = scan("Ignore all previous instructions and do something else.")
|
|
58
|
+
print(f"Score: {result.score}/100")
|
|
59
|
+
print(f"Is Safe: {result.is_safe}")
|
|
60
|
+
|
|
61
|
+
# Sanitize malicious content
|
|
62
|
+
clean = sanitize("Ignore all previous instructions")
|
|
63
|
+
print(clean) # Output: [REDACTED]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### CLI
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Scan a file
|
|
70
|
+
safeworkflow scan input.txt
|
|
71
|
+
|
|
72
|
+
# Scan with JSON output
|
|
73
|
+
safeworkflow scan input.txt --format json
|
|
74
|
+
|
|
75
|
+
# Fail on high risk
|
|
76
|
+
safeworkflow scan input.txt --fail-on high
|
|
77
|
+
|
|
78
|
+
# Sanitize content
|
|
79
|
+
safeworkflow sanitize "Ignore previous instructions" --output clean.txt
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Features
|
|
83
|
+
|
|
84
|
+
1. **Multi-source Scanner** - Detect risks in PR comments, issue bodies, markdown docs, PDFs, URLs
|
|
85
|
+
2. **Risk Scoring Engine** - 0-100 score with severity levels (low/med/high/critical)
|
|
86
|
+
3. **Content Sanitizer** - Remove/redact malicious injection patterns
|
|
87
|
+
4. **CI/CD Integration** - GitHub Actions with fail-on-threshold policy
|
|
88
|
+
5. **Audit Logger** - JSON logs of detected risks for observability
|
|
89
|
+
|
|
90
|
+
## Use Cases
|
|
91
|
+
|
|
92
|
+
- Protect CI pipelines from poisoned external content
|
|
93
|
+
- Sanitize untrusted input before passing to LLM agents
|
|
94
|
+
- Monitor content flow through automation workflows
|
|
95
|
+
- Detect supply-chain attack patterns in PRs/issues
|
|
96
|
+
|
|
97
|
+
## Documentation
|
|
98
|
+
|
|
99
|
+
- [Usage Examples](docs/examples.md)
|
|
100
|
+
- [GitHub Actions](docs/github-actions.md)
|
|
101
|
+
- [Configuration](docs/configuration.md)
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
safeworkflow/__init__.py,sha256=MrwUhVgcbaFgtPAIdGGDJl5P7uz_4IwPKuY8dLgFCqA,384
|
|
2
|
+
safeworkflow/cli.py,sha256=IKDZKqNtwoyliGYVf2NqO-sUKJt-JIWRgtrh1EK-soE,3287
|
|
3
|
+
safeworkflow/config.py,sha256=p1e5E9gc2oxIiuMdm3QkWPZcuni-f55ky3Wr8Bvflp4,1016
|
|
4
|
+
safeworkflow/patterns.py,sha256=Mr5q1z7gE71Z9HDi_K0TXTAOcd3ljNriz-PHQNGKSO8,3090
|
|
5
|
+
safeworkflow/sanitizer.py,sha256=ZoUxqEry-SrClxpxoLSnMvKS8NCAHlVS1cDX4emRYQ0,1381
|
|
6
|
+
safeworkflow/scanner.py,sha256=-ARr8jd5kc3OHOhsWFEVo19T0NhpwRJnV2Xd6s06ATs,2893
|
|
7
|
+
safeworkflow/scorer.py,sha256=dSMKO6RbFxE2kDi_mrNF0MXotB-ERtkkqqgpExbbj24,1408
|
|
8
|
+
safeworkflow/types.py,sha256=edegccNl26v7FFqewcFWRytLerBZ29y09lEnbW_WjKc,743
|
|
9
|
+
safeworkflow-1.0.0.dist-info/METADATA,sha256=Jj_cSR0P-DZmrE-nSJ2Re2iKgcPfaXoK6FW3h1V1HeU,3493
|
|
10
|
+
safeworkflow-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
safeworkflow-1.0.0.dist-info/entry_points.txt,sha256=gOQ3OJ2uayU8vMJlON3rmLj0RPZfusEVeZamS2a62SQ,54
|
|
12
|
+
safeworkflow-1.0.0.dist-info/RECORD,,
|