pyossmtool 0.0.1__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.
- pyossmtool/__init__.py +3 -0
- pyossmtool/bundle/catalog/checks/bandit.scan.yaml +13 -0
- pyossmtool/bundle/catalog/checks/codespell.spelling.yaml +12 -0
- pyossmtool/bundle/catalog/checks/deadcode.scan.yaml +13 -0
- pyossmtool/bundle/catalog/checks/gate.acronym-allowlist.yaml +14 -0
- pyossmtool/bundle/catalog/checks/gate.folder-breadth.yaml +14 -0
- pyossmtool/bundle/catalog/checks/gate.module-private-vars.yaml +14 -0
- pyossmtool/bundle/catalog/checks/gate.module-size.yaml +14 -0
- pyossmtool/bundle/catalog/checks/gitleaks.secrets.yaml +21 -0
- pyossmtool/bundle/catalog/checks/hadolint.dockerfile.yaml +16 -0
- pyossmtool/bundle/catalog/checks/jscpd.duplication.yaml +19 -0
- pyossmtool/bundle/catalog/checks/markdownlint.check.yaml +17 -0
- pyossmtool/bundle/catalog/checks/mdformat.apply.yaml +10 -0
- pyossmtool/bundle/catalog/checks/mdformat.format.yaml +14 -0
- pyossmtool/bundle/catalog/checks/mutmut.run.yaml +13 -0
- pyossmtool/bundle/catalog/checks/pydeps.cycles.yaml +14 -0
- pyossmtool/bundle/catalog/checks/pytest.coverage.yaml +19 -0
- pyossmtool/bundle/catalog/checks/pytest.test.yaml +14 -0
- pyossmtool/bundle/catalog/checks/radon.cc.yaml +13 -0
- pyossmtool/bundle/catalog/checks/radon.mi.yaml +13 -0
- pyossmtool/bundle/catalog/checks/ruff.format.apply.yaml +11 -0
- pyossmtool/bundle/catalog/checks/ruff.format.yaml +14 -0
- pyossmtool/bundle/catalog/checks/ruff.lint.yaml +15 -0
- pyossmtool/bundle/catalog/checks/ruff.unused.yaml +17 -0
- pyossmtool/bundle/catalog/checks/semgrep.scan.yaml +18 -0
- pyossmtool/bundle/catalog/checks/shellcheck.yaml +13 -0
- pyossmtool/bundle/catalog/checks/shfmt.apply.yaml +11 -0
- pyossmtool/bundle/catalog/checks/shfmt.format.yaml +14 -0
- pyossmtool/bundle/catalog/checks/sourcery.review.yaml +14 -0
- pyossmtool/bundle/catalog/checks/ty.check.yaml +15 -0
- pyossmtool/bundle/catalog/checks/vulture.scan.yaml +16 -0
- pyossmtool/bundle/catalog/checks/yamlfmt.apply.yaml +10 -0
- pyossmtool/bundle/catalog/checks/yamlfmt.format.yaml +14 -0
- pyossmtool/bundle/catalog/checks/yamllint.check.yaml +16 -0
- pyossmtool/bundle/catalog/tools/bandit.yaml +20 -0
- pyossmtool/bundle/catalog/tools/codespell.yaml +17 -0
- pyossmtool/bundle/catalog/tools/deadcode.yaml +13 -0
- pyossmtool/bundle/catalog/tools/gitleaks.yaml +9 -0
- pyossmtool/bundle/catalog/tools/hadolint.yaml +11 -0
- pyossmtool/bundle/catalog/tools/jscpd.yaml +15 -0
- pyossmtool/bundle/catalog/tools/markdownlint.yaml +18 -0
- pyossmtool/bundle/catalog/tools/mdformat.yaml +13 -0
- pyossmtool/bundle/catalog/tools/mutmut.yaml +10 -0
- pyossmtool/bundle/catalog/tools/pydeps.yaml +10 -0
- pyossmtool/bundle/catalog/tools/pytest.yaml +12 -0
- pyossmtool/bundle/catalog/tools/radon.yaml +10 -0
- pyossmtool/bundle/catalog/tools/ruff.yaml +26 -0
- pyossmtool/bundle/catalog/tools/script.yaml +8 -0
- pyossmtool/bundle/catalog/tools/semgrep.yaml +21 -0
- pyossmtool/bundle/catalog/tools/shellcheck.yaml +10 -0
- pyossmtool/bundle/catalog/tools/shfmt.yaml +13 -0
- pyossmtool/bundle/catalog/tools/sourcery.yaml +10 -0
- pyossmtool/bundle/catalog/tools/ty.yaml +20 -0
- pyossmtool/bundle/catalog/tools/vulture.yaml +13 -0
- pyossmtool/bundle/catalog/tools/yamlfmt.yaml +24 -0
- pyossmtool/bundle/catalog/tools/yamllint.yaml +24 -0
- pyossmtool/bundle/defaults/allowlists/acronyms.yaml +10 -0
- pyossmtool/bundle/defaults/allowlists/folder-breadth.txt +1 -0
- pyossmtool/bundle/defaults/allowlists/module-private-vars.txt +1 -0
- pyossmtool/bundle/defaults/allowlists/module-size.txt +1 -0
- pyossmtool/bundle/defaults/configs/bandit.yaml +5 -0
- pyossmtool/bundle/defaults/configs/gates/acronym-allowlist.yaml +4 -0
- pyossmtool/bundle/defaults/configs/gates/folder-breadth.yaml +12 -0
- pyossmtool/bundle/defaults/configs/gates/module-private-vars.yaml +4 -0
- pyossmtool/bundle/defaults/configs/gates/module-size.yaml +6 -0
- pyossmtool/bundle/defaults/configs/markdownlint.json +8 -0
- pyossmtool/bundle/defaults/configs/ruff.toml +8 -0
- pyossmtool/bundle/defaults/configs/ty.toml +5 -0
- pyossmtool/bundle/defaults/configs/yamlfmt.yaml +4 -0
- pyossmtool/bundle/defaults/configs/yamllint.yaml +12 -0
- pyossmtool/bundle/defaults/gates/README.md +150 -0
- pyossmtool/bundle/defaults/gates/acronym-allowlist.sh +15 -0
- pyossmtool/bundle/defaults/gates/folder-breadth.sh +22 -0
- pyossmtool/bundle/defaults/gates/lib.sh +116 -0
- pyossmtool/bundle/defaults/gates/module-private-vars.sh +77 -0
- pyossmtool/bundle/defaults/gates/module-size.sh +60 -0
- pyossmtool/bundle/defaults/semgrep/python-quality.yml +19 -0
- pyossmtool/bundle/suites/demo.yaml +11 -0
- pyossmtool/bundle/suites/extended.yaml +34 -0
- pyossmtool/bundle/suites/format.yaml +10 -0
- pyossmtool/bundle/suites/formatting.yaml +10 -0
- pyossmtool/bundle/suites/policy.yaml +11 -0
- pyossmtool/bundle/suites/python-quality.yaml +9 -0
- pyossmtool/bundle/suites/reslab-parity.yaml +41 -0
- pyossmtool/bundle/suites/standard.yaml +45 -0
- pyossmtool/cli.py +233 -0
- pyossmtool/config_resolver.py +117 -0
- pyossmtool/discovery.py +50 -0
- pyossmtool/gate_config.py +119 -0
- pyossmtool/gates.py +102 -0
- pyossmtool/ignore.py +270 -0
- pyossmtool/ignore_materialize.py +184 -0
- pyossmtool/installer.py +119 -0
- pyossmtool/models.py +279 -0
- pyossmtool/parsers/__init__.py +76 -0
- pyossmtool/parsers/analysis.py +329 -0
- pyossmtool/parsers/base.py +29 -0
- pyossmtool/parsers/common.py +57 -0
- pyossmtool/parsers/format_text.py +67 -0
- pyossmtool/parsers/gates.py +92 -0
- pyossmtool/parsers/patterns.py +78 -0
- pyossmtool/parsers/prose.py +193 -0
- pyossmtool/parsers/ruff.py +103 -0
- pyossmtool/parsers/shell.py +83 -0
- pyossmtool/policy/__init__.py +0 -0
- pyossmtool/policy/acronym_allowlist.py +322 -0
- pyossmtool/policy/folder_breadth.py +306 -0
- pyossmtool/registry.py +72 -0
- pyossmtool/reporter.py +87 -0
- pyossmtool/resolver.py +146 -0
- pyossmtool/runner.py +482 -0
- pyossmtool/runner_script.py +131 -0
- pyossmtool/target_expand.py +112 -0
- pyossmtool-0.0.1.dist-info/METADATA +94 -0
- pyossmtool-0.0.1.dist-info/RECORD +119 -0
- pyossmtool-0.0.1.dist-info/WHEEL +5 -0
- pyossmtool-0.0.1.dist-info/entry_points.txt +2 -0
- pyossmtool-0.0.1.dist-info/licenses/LICENSE +21 -0
- pyossmtool-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Per-tool ignore pattern materialization driven by ToolDef.ignore."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from pyossmtool.ignore import EffectiveIgnores, ToolIgnoreMaterial, _all_patterns, _merge_yaml_config
|
|
11
|
+
from pyossmtool.models import ExtendFrom, IgnoreKind, ToolDef, ToolIgnoreSpec
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def materialize_for_tool(
|
|
15
|
+
tool: ToolDef,
|
|
16
|
+
ignores: EffectiveIgnores,
|
|
17
|
+
*,
|
|
18
|
+
project_root: Path,
|
|
19
|
+
check_id: str,
|
|
20
|
+
base_config_path: Path | None = None,
|
|
21
|
+
) -> ToolIgnoreMaterial:
|
|
22
|
+
patterns = _all_patterns(ignores)
|
|
23
|
+
if not patterns or tool.ignore is None:
|
|
24
|
+
return ToolIgnoreMaterial()
|
|
25
|
+
|
|
26
|
+
cache_dir = project_root / ".pyossmtool" / "cache" / "ignores" / check_id
|
|
27
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
return _materialize(tool.ignore, patterns, project_root, cache_dir, base_config_path)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _materialize(
|
|
32
|
+
spec: ToolIgnoreSpec,
|
|
33
|
+
patterns: list[str],
|
|
34
|
+
project_root: Path,
|
|
35
|
+
cache_dir: Path,
|
|
36
|
+
base_config_path: Path | None,
|
|
37
|
+
) -> ToolIgnoreMaterial:
|
|
38
|
+
if spec.kind == IgnoreKind.FLAG_PAIRS:
|
|
39
|
+
return _flag_pairs(spec, patterns)
|
|
40
|
+
if spec.kind == IgnoreKind.PREFIX:
|
|
41
|
+
return _prefix_flags(spec, patterns)
|
|
42
|
+
if spec.kind == IgnoreKind.SKIP_FILE:
|
|
43
|
+
return _skip_file(spec, patterns, cache_dir)
|
|
44
|
+
if spec.kind == IgnoreKind.CONFIG_OVERLAY:
|
|
45
|
+
return _config_overlay(spec, patterns, project_root, cache_dir, base_config_path)
|
|
46
|
+
return ToolIgnoreMaterial()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _flag_pairs(spec: ToolIgnoreSpec, patterns: list[str]) -> ToolIgnoreMaterial:
|
|
50
|
+
if not spec.flag:
|
|
51
|
+
return ToolIgnoreMaterial()
|
|
52
|
+
argv: list[str] = []
|
|
53
|
+
for pattern in patterns:
|
|
54
|
+
argv.extend([spec.flag, pattern])
|
|
55
|
+
return ToolIgnoreMaterial(argv=argv, post_subcommand=spec.post_subcommand)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _prefix_flags(spec: ToolIgnoreSpec, patterns: list[str]) -> ToolIgnoreMaterial:
|
|
59
|
+
if not spec.prefix:
|
|
60
|
+
return ToolIgnoreMaterial()
|
|
61
|
+
return ToolIgnoreMaterial(
|
|
62
|
+
argv=[f"{spec.prefix}{pattern}" for pattern in patterns],
|
|
63
|
+
post_subcommand=spec.post_subcommand,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _skip_file(spec: ToolIgnoreSpec, patterns: list[str], cache_dir: Path) -> ToolIgnoreMaterial:
|
|
68
|
+
filename = spec.file or "ignores.skip"
|
|
69
|
+
skip_file = cache_dir / filename
|
|
70
|
+
skip_file.write_text("\n".join(patterns) + "\n", encoding="utf-8")
|
|
71
|
+
flag = spec.flag or "--skip"
|
|
72
|
+
return ToolIgnoreMaterial(
|
|
73
|
+
argv=[flag, str(skip_file)],
|
|
74
|
+
skip_file=skip_file,
|
|
75
|
+
post_subcommand=spec.post_subcommand,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _config_overlay(
|
|
80
|
+
spec: ToolIgnoreSpec,
|
|
81
|
+
patterns: list[str],
|
|
82
|
+
project_root: Path,
|
|
83
|
+
cache_dir: Path,
|
|
84
|
+
base_config_path: Path | None,
|
|
85
|
+
) -> ToolIgnoreMaterial:
|
|
86
|
+
filename = spec.file or f"ignores.{spec.format or 'txt'}"
|
|
87
|
+
config_path = cache_dir / filename
|
|
88
|
+
body = _overlay_body(spec, patterns, project_root, base_config_path)
|
|
89
|
+
if body is None:
|
|
90
|
+
return ToolIgnoreMaterial()
|
|
91
|
+
config_path.write_text(body, encoding="utf-8")
|
|
92
|
+
flag = spec.flag or "--config"
|
|
93
|
+
return ToolIgnoreMaterial(
|
|
94
|
+
argv=[flag, str(config_path)],
|
|
95
|
+
config_path=config_path,
|
|
96
|
+
post_subcommand=spec.post_subcommand,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _overlay_body(
|
|
101
|
+
spec: ToolIgnoreSpec,
|
|
102
|
+
patterns: list[str],
|
|
103
|
+
project_root: Path,
|
|
104
|
+
base_config_path: Path | None,
|
|
105
|
+
) -> str | None:
|
|
106
|
+
if spec.format == "toml":
|
|
107
|
+
return _toml_overlay(spec, patterns, project_root, base_config_path)
|
|
108
|
+
if spec.format == "yaml":
|
|
109
|
+
return _yaml_overlay(spec, patterns, base_config_path)
|
|
110
|
+
if spec.format == "json":
|
|
111
|
+
return _json_overlay(spec, patterns)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _toml_overlay(
|
|
116
|
+
spec: ToolIgnoreSpec,
|
|
117
|
+
patterns: list[str],
|
|
118
|
+
project_root: Path,
|
|
119
|
+
base_config_path: Path | None,
|
|
120
|
+
) -> str:
|
|
121
|
+
lines: list[str] = []
|
|
122
|
+
extend_line = _toml_extend_line(spec, project_root, base_config_path)
|
|
123
|
+
if extend_line:
|
|
124
|
+
lines.append(extend_line)
|
|
125
|
+
if spec.toml_table:
|
|
126
|
+
lines.append(f"[{spec.toml_table}]")
|
|
127
|
+
key = spec.key or "exclude"
|
|
128
|
+
lines.append(f"{key} = [")
|
|
129
|
+
lines.extend(f' "{pattern}",' for pattern in patterns)
|
|
130
|
+
lines.append("]")
|
|
131
|
+
return "\n".join(lines) + "\n"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _toml_extend_line(
|
|
135
|
+
spec: ToolIgnoreSpec,
|
|
136
|
+
project_root: Path,
|
|
137
|
+
base_config_path: Path | None,
|
|
138
|
+
) -> str | None:
|
|
139
|
+
extend_key = spec.extend_key or "extend"
|
|
140
|
+
if spec.extend_from == ExtendFrom.BASE_CONFIG:
|
|
141
|
+
return _extend_base_only(extend_key, base_config_path)
|
|
142
|
+
if spec.extend_from == ExtendFrom.BASE_OR_PYPROJECT:
|
|
143
|
+
return _extend_base_or_pyproject(extend_key, project_root, base_config_path)
|
|
144
|
+
if spec.extend_from == ExtendFrom.PYPROJECT:
|
|
145
|
+
return _extend_pyproject_list(extend_key, project_root)
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _extend_base_only(extend_key: str, base_config_path: Path | None) -> str | None:
|
|
150
|
+
if base_config_path and base_config_path.exists():
|
|
151
|
+
return f'{extend_key} = "{base_config_path}"'
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _extend_base_or_pyproject(extend_key: str, project_root: Path, base_config_path: Path | None) -> str | None:
|
|
156
|
+
base = _extend_base_only(extend_key, base_config_path)
|
|
157
|
+
if base:
|
|
158
|
+
return base
|
|
159
|
+
pyproject = project_root / "pyproject.toml"
|
|
160
|
+
if pyproject.exists():
|
|
161
|
+
return f'{extend_key} = "{pyproject}"'
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _extend_pyproject_list(extend_key: str, project_root: Path) -> str | None:
|
|
166
|
+
pyproject = project_root / "pyproject.toml"
|
|
167
|
+
if pyproject.exists():
|
|
168
|
+
return f'{extend_key} = ["{pyproject}"]'
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _yaml_overlay(spec: ToolIgnoreSpec, patterns: list[str], base_config_path: Path | None) -> str:
|
|
173
|
+
key = spec.key or "exclude"
|
|
174
|
+
if spec.key_style == "block":
|
|
175
|
+
overlay = {key: "\n".join(patterns) + ("\n" if patterns else "")}
|
|
176
|
+
else:
|
|
177
|
+
overlay = {key: patterns}
|
|
178
|
+
merged = _merge_yaml_config(base_config_path, overlay)
|
|
179
|
+
return yaml.safe_dump(merged, sort_keys=False)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _json_overlay(spec: ToolIgnoreSpec, patterns: list[str]) -> str:
|
|
183
|
+
key = spec.key or "ignore"
|
|
184
|
+
return json.dumps({key: patterns}, indent=2) + "\n"
|
pyossmtool/installer.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Install quality tool dependencies into managed environments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from pyossmtool.models import EnvMode, InstallMethod, SuiteDef, ToolDef
|
|
11
|
+
from pyossmtool.registry import Registry
|
|
12
|
+
from pyossmtool.resolver import BinaryResolver
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Installer:
|
|
16
|
+
def __init__(self, registry: Registry, project_root: Path | None = None) -> None:
|
|
17
|
+
self.registry = registry
|
|
18
|
+
self.project_root = (project_root or Path.cwd()).resolve()
|
|
19
|
+
self.resolver = BinaryResolver(self.project_root)
|
|
20
|
+
self.managed_venv = self.project_root / ".pyossmtool" / "tools" / "venv"
|
|
21
|
+
|
|
22
|
+
def tools_for_suite(self, suite: SuiteDef) -> list[ToolDef]:
|
|
23
|
+
tool_ids: set[str] = set()
|
|
24
|
+
for check_ref in suite.checks:
|
|
25
|
+
check = self.registry.get_check(check_ref.id)
|
|
26
|
+
tool_ids.add(check.tool)
|
|
27
|
+
return [self.registry.get_tool(tool_id) for tool_id in sorted(tool_ids)]
|
|
28
|
+
|
|
29
|
+
def install_suite(self, suite_id: str) -> None:
|
|
30
|
+
suite = self.registry.get_suite(suite_id)
|
|
31
|
+
for tool in self.tools_for_suite(suite):
|
|
32
|
+
self.install_tool(tool)
|
|
33
|
+
|
|
34
|
+
def install_tool(self, tool: ToolDef) -> None:
|
|
35
|
+
method = tool.install.method
|
|
36
|
+
handlers = {
|
|
37
|
+
InstallMethod.SKIP: lambda _tool: None,
|
|
38
|
+
InstallMethod.SYSTEM: self._warn_missing_system_tool,
|
|
39
|
+
InstallMethod.PIP: self._install_pip_tool,
|
|
40
|
+
InstallMethod.NPM: self._install_npm_tool,
|
|
41
|
+
}
|
|
42
|
+
handler = handlers.get(method)
|
|
43
|
+
if handler is None:
|
|
44
|
+
raise RuntimeError(f"Unsupported install method: {method}")
|
|
45
|
+
handler(tool)
|
|
46
|
+
|
|
47
|
+
def _install_npm_tool(self, tool: ToolDef) -> None:
|
|
48
|
+
if self._binary_available(tool):
|
|
49
|
+
return
|
|
50
|
+
self._install_npm(tool)
|
|
51
|
+
self._require_binary(tool)
|
|
52
|
+
|
|
53
|
+
def _warn_missing_system_tool(self, tool: ToolDef) -> None:
|
|
54
|
+
if self.resolver._resolve_system(tool.binary) is not None:
|
|
55
|
+
return
|
|
56
|
+
print(
|
|
57
|
+
f"WARN: system tool '{tool.binary}' not found; install manually to enable related checks",
|
|
58
|
+
file=sys.stderr,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def _install_pip_tool(self, tool: ToolDef) -> None:
|
|
62
|
+
self._ensure_managed_venv()
|
|
63
|
+
self._install_pip(tool)
|
|
64
|
+
self._require_binary(tool)
|
|
65
|
+
|
|
66
|
+
def _require_binary(self, tool: ToolDef) -> None:
|
|
67
|
+
if self._binary_available(tool):
|
|
68
|
+
return
|
|
69
|
+
raise RuntimeError(f"Failed to install tool '{tool.id}' ({tool.binary})")
|
|
70
|
+
|
|
71
|
+
def _binary_available(self, tool: ToolDef) -> bool:
|
|
72
|
+
try:
|
|
73
|
+
self.resolver.resolve(tool, mode=EnvMode.MANAGED)
|
|
74
|
+
return True
|
|
75
|
+
except FileNotFoundError:
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
def _managed_python(self) -> str:
|
|
79
|
+
if sys.version_info < (3, 14):
|
|
80
|
+
return sys.executable
|
|
81
|
+
for candidate in ("python3.13", "python3.12", "python3.11"):
|
|
82
|
+
found = shutil.which(candidate)
|
|
83
|
+
if found:
|
|
84
|
+
return found
|
|
85
|
+
return sys.executable
|
|
86
|
+
|
|
87
|
+
def _ensure_managed_venv(self) -> None:
|
|
88
|
+
python_bin = self.managed_venv / "bin" / "python"
|
|
89
|
+
if python_bin.exists():
|
|
90
|
+
version = subprocess.run(
|
|
91
|
+
[str(python_bin), "--version"],
|
|
92
|
+
capture_output=True,
|
|
93
|
+
text=True,
|
|
94
|
+
check=True,
|
|
95
|
+
).stdout
|
|
96
|
+
if "3.14" in version:
|
|
97
|
+
shutil.rmtree(self.managed_venv)
|
|
98
|
+
if (self.managed_venv / "bin" / "python").exists():
|
|
99
|
+
return
|
|
100
|
+
self.managed_venv.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
subprocess.run([self._managed_python(), "-m", "venv", str(self.managed_venv)], check=True)
|
|
102
|
+
|
|
103
|
+
def _install_pip(self, tool: ToolDef) -> None:
|
|
104
|
+
pip = self.managed_venv / "bin" / "pip"
|
|
105
|
+
packages = (tool.install.package or tool.id).split()
|
|
106
|
+
subprocess.run([str(pip), "install", "--upgrade", *packages], check=True)
|
|
107
|
+
|
|
108
|
+
def _install_npm(self, tool: ToolDef) -> None:
|
|
109
|
+
package = tool.install.package or tool.id
|
|
110
|
+
npm_root = self.project_root / ".pyossmtool" / "tools" / "npm"
|
|
111
|
+
npm_root.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
package_json = npm_root / "package.json"
|
|
113
|
+
if not package_json.exists():
|
|
114
|
+
package_json.write_text('{"name":"pyossmtool-tools","private":true}\n', encoding="utf-8")
|
|
115
|
+
subprocess.run(
|
|
116
|
+
["npm", "install", "--no-save", "--prefix", str(npm_root), package],
|
|
117
|
+
check=True,
|
|
118
|
+
cwd=npm_root,
|
|
119
|
+
)
|
pyossmtool/models.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Pydantic models for tools, checks, suites, and failure reports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
10
|
+
|
|
11
|
+
SCHEMA_VERSION = "1.0"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InstallMethod(StrEnum):
|
|
15
|
+
PIP = "pip"
|
|
16
|
+
NPM = "npm"
|
|
17
|
+
SYSTEM = "system"
|
|
18
|
+
SKIP = "skip"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EnvMode(StrEnum):
|
|
22
|
+
MANAGED = "managed"
|
|
23
|
+
PROJECT = "project"
|
|
24
|
+
AUTO = "auto"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Severity(StrEnum):
|
|
28
|
+
ERROR = "error"
|
|
29
|
+
WARNING = "warning"
|
|
30
|
+
INFO = "info"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class InstallSpec(BaseModel):
|
|
34
|
+
method: InstallMethod
|
|
35
|
+
package: str | None = None
|
|
36
|
+
version: str | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ToolConfigSpec(BaseModel):
|
|
40
|
+
"""Declarative config discovery for a tool (repo-native vs bundled)."""
|
|
41
|
+
|
|
42
|
+
flag: str | None = None
|
|
43
|
+
pass_flag: bool | None = None
|
|
44
|
+
bundled: str | None = None
|
|
45
|
+
bundled_path: str | None = None
|
|
46
|
+
repo_files: list[str] = Field(default_factory=list)
|
|
47
|
+
repo_dirs: list[str] = Field(default_factory=list)
|
|
48
|
+
pyproject: list[str] = Field(default_factory=list)
|
|
49
|
+
use_pyproject_as_path: bool = False
|
|
50
|
+
|
|
51
|
+
def should_pass_flag(self) -> bool:
|
|
52
|
+
if self.pass_flag is not None:
|
|
53
|
+
return self.pass_flag
|
|
54
|
+
return bool(self.flag)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class IgnoreKind(StrEnum):
|
|
58
|
+
FLAG_PAIRS = "flag_pairs"
|
|
59
|
+
PREFIX = "prefix"
|
|
60
|
+
SKIP_FILE = "skip_file"
|
|
61
|
+
CONFIG_OVERLAY = "config_overlay"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ExtendFrom(StrEnum):
|
|
65
|
+
NONE = "none"
|
|
66
|
+
BASE_CONFIG = "base_config"
|
|
67
|
+
BASE_OR_PYPROJECT = "base_or_pyproject"
|
|
68
|
+
PYPROJECT = "pyproject"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ToolIgnoreSpec(BaseModel):
|
|
72
|
+
"""How this tool consumes unified ignore patterns."""
|
|
73
|
+
|
|
74
|
+
kind: IgnoreKind
|
|
75
|
+
flag: str | None = None
|
|
76
|
+
prefix: str | None = None
|
|
77
|
+
post_subcommand: bool = False
|
|
78
|
+
format: str | None = None
|
|
79
|
+
file: str | None = None
|
|
80
|
+
key: str | None = None
|
|
81
|
+
key_style: str = "list"
|
|
82
|
+
extend_from: ExtendFrom = ExtendFrom.NONE
|
|
83
|
+
extend_key: str | None = None
|
|
84
|
+
toml_table: str | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ToolDef(BaseModel):
|
|
88
|
+
id: str
|
|
89
|
+
name: str
|
|
90
|
+
description: str
|
|
91
|
+
install: InstallSpec
|
|
92
|
+
binary: str
|
|
93
|
+
documentation_url: str | None = None
|
|
94
|
+
files: list[str] = Field(default_factory=list)
|
|
95
|
+
config: ToolConfigSpec | None = None
|
|
96
|
+
ignore: ToolIgnoreSpec | None = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class SuccessCriteria(BaseModel):
|
|
100
|
+
exit_codes: list[int] = Field(default_factory=lambda: [0])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class CheckPolicy(BaseModel):
|
|
104
|
+
max_complexity_rank: str | None = None
|
|
105
|
+
max_duplication_percent: float | None = None
|
|
106
|
+
min_coverage_percent: float | None = None
|
|
107
|
+
coverage_source: str | None = None
|
|
108
|
+
min_confidence: int | None = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ConfigMode(StrEnum):
|
|
112
|
+
AUTO = "auto"
|
|
113
|
+
BUNDLED = "bundled"
|
|
114
|
+
PATHS = "paths"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class CheckMode(StrEnum):
|
|
118
|
+
CHECK = "check"
|
|
119
|
+
FORMAT = "format"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ConfigSpec(BaseModel):
|
|
123
|
+
mode: ConfigMode = ConfigMode.AUTO
|
|
124
|
+
paths: dict[str, str] = Field(default_factory=dict)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class IgnoreSpec(BaseModel):
|
|
128
|
+
"""Repo-wide ignore rules merged across suite, project, and per-check layers."""
|
|
129
|
+
|
|
130
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
131
|
+
|
|
132
|
+
ignore_profile: list[str] = Field(default_factory=list, alias="ignore-profile")
|
|
133
|
+
ignore_paths: list[str] = Field(default_factory=list, alias="ignore-paths")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class GateConfigSpec(BaseModel):
|
|
137
|
+
"""Declarative policy-config discovery for script gates."""
|
|
138
|
+
|
|
139
|
+
bundled: str | None = None
|
|
140
|
+
project_file: str | None = None
|
|
141
|
+
allowlist_bundled: str | None = None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class CheckDef(BaseModel):
|
|
145
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
146
|
+
|
|
147
|
+
id: str
|
|
148
|
+
tool: str
|
|
149
|
+
name: str
|
|
150
|
+
description: str
|
|
151
|
+
argv: list[str] = Field(default_factory=list)
|
|
152
|
+
parser: str
|
|
153
|
+
mode: CheckMode = CheckMode.CHECK
|
|
154
|
+
script: str | None = None
|
|
155
|
+
include: list[str] = Field(default_factory=list)
|
|
156
|
+
config: GateConfigSpec | None = None
|
|
157
|
+
success: SuccessCriteria = Field(default_factory=SuccessCriteria)
|
|
158
|
+
policy: CheckPolicy | None = None
|
|
159
|
+
remediation: dict[str, Any] = Field(default_factory=dict)
|
|
160
|
+
output_file: str | None = None
|
|
161
|
+
ignore_profile: list[str] = Field(default_factory=list, alias="ignore-profile")
|
|
162
|
+
ignore_paths: list[str] = Field(default_factory=list, alias="ignore-paths")
|
|
163
|
+
|
|
164
|
+
@model_validator(mode="after")
|
|
165
|
+
def validate_script_gate(self) -> CheckDef:
|
|
166
|
+
if self.tool == "script" and not self.script:
|
|
167
|
+
raise ValueError(f"Check '{self.id}' uses tool 'script' but has no script path")
|
|
168
|
+
return self
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class SuiteCheckRef(BaseModel):
|
|
172
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
173
|
+
|
|
174
|
+
id: str
|
|
175
|
+
target: str | None = None
|
|
176
|
+
ignore_profile: list[str] = Field(default_factory=list, alias="ignore-profile")
|
|
177
|
+
ignore_paths: list[str] = Field(default_factory=list, alias="ignore-paths")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class SuiteDef(BaseModel):
|
|
181
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
182
|
+
|
|
183
|
+
id: str
|
|
184
|
+
name: str
|
|
185
|
+
description: str
|
|
186
|
+
env: EnvMode = EnvMode.AUTO
|
|
187
|
+
checks: list[SuiteCheckRef]
|
|
188
|
+
target: str = "."
|
|
189
|
+
ignore_profile: list[str] = Field(default_factory=list, alias="ignore-profile")
|
|
190
|
+
ignore_paths: list[str] = Field(default_factory=list, alias="ignore-paths")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class ProjectConfig(BaseModel):
|
|
194
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
195
|
+
|
|
196
|
+
suite: str
|
|
197
|
+
env: EnvMode = EnvMode.AUTO
|
|
198
|
+
target: str = "."
|
|
199
|
+
checks: list[SuiteCheckRef] = Field(default_factory=list)
|
|
200
|
+
configs: ConfigSpec = Field(default_factory=ConfigSpec)
|
|
201
|
+
ignore_profile: list[str] = Field(default_factory=list, alias="ignore-profile")
|
|
202
|
+
ignore_paths: list[str] = Field(default_factory=list, alias="ignore-paths")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class Location(BaseModel):
|
|
206
|
+
file: str
|
|
207
|
+
line: int | None = None
|
|
208
|
+
column: int | None = None
|
|
209
|
+
end_line: int | None = None
|
|
210
|
+
end_column: int | None = None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class FixHint(BaseModel):
|
|
214
|
+
description: str | None = None
|
|
215
|
+
command: str | None = None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class Finding(BaseModel):
|
|
219
|
+
rule_id: str
|
|
220
|
+
severity: Severity
|
|
221
|
+
message: str
|
|
222
|
+
location: Location | None = None
|
|
223
|
+
snippet: str | None = None
|
|
224
|
+
fix: FixHint | None = None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class FailureSummary(BaseModel):
|
|
228
|
+
finding_count: int
|
|
229
|
+
by_severity: dict[str, int] = Field(default_factory=dict)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class Remediation(BaseModel):
|
|
233
|
+
docs_url: str | None = None
|
|
234
|
+
suggested_commands: list[str] = Field(default_factory=list)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class FailureArtifacts(BaseModel):
|
|
238
|
+
raw_stdout: str | None = None
|
|
239
|
+
raw_stderr: str | None = None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class FailureReport(BaseModel):
|
|
243
|
+
schema_version: str = SCHEMA_VERSION
|
|
244
|
+
check_id: str
|
|
245
|
+
tool_id: str
|
|
246
|
+
status: str = "failed"
|
|
247
|
+
suite_id: str | None = None
|
|
248
|
+
target: str
|
|
249
|
+
started_at: datetime
|
|
250
|
+
duration_ms: int
|
|
251
|
+
summary: FailureSummary
|
|
252
|
+
findings: list[Finding]
|
|
253
|
+
remediation: Remediation = Field(default_factory=Remediation)
|
|
254
|
+
artifacts: FailureArtifacts = Field(default_factory=FailureArtifacts)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class CheckResult(BaseModel):
|
|
258
|
+
check_id: str
|
|
259
|
+
passed: bool
|
|
260
|
+
report_path: str | None = None
|
|
261
|
+
error: str | None = None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class SuiteResult(BaseModel):
|
|
265
|
+
suite_id: str
|
|
266
|
+
passed: bool
|
|
267
|
+
results: list[CheckResult] = Field(default_factory=list)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def utc_now() -> datetime:
|
|
271
|
+
return datetime.now(UTC)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def summarize_findings(findings: list[Finding]) -> FailureSummary:
|
|
275
|
+
by_severity: dict[str, int] = {}
|
|
276
|
+
for finding in findings:
|
|
277
|
+
key = finding.severity.value
|
|
278
|
+
by_severity[key] = by_severity.get(key, 0) + 1
|
|
279
|
+
return FailureSummary(finding_count=len(findings), by_severity=by_severity)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Parse native tool output into normalized Finding objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pyossmtool.models import CheckDef, Finding
|
|
6
|
+
from pyossmtool.parsers.analysis import (
|
|
7
|
+
BanditParser,
|
|
8
|
+
DeadcodeParser,
|
|
9
|
+
JscpdParser,
|
|
10
|
+
PydepsCyclesParser,
|
|
11
|
+
PytestParser,
|
|
12
|
+
RadonCcParser,
|
|
13
|
+
RadonMiParser,
|
|
14
|
+
SemgrepParser,
|
|
15
|
+
TyParser,
|
|
16
|
+
VultureParser,
|
|
17
|
+
)
|
|
18
|
+
from pyossmtool.parsers.base import REGISTRY
|
|
19
|
+
from pyossmtool.parsers.format_text import MdformatParser, YamlfmtParser
|
|
20
|
+
from pyossmtool.parsers.gates import CliTextParser, GateJsonParser, NoopParser, ScriptTextParser
|
|
21
|
+
from pyossmtool.parsers.prose import (
|
|
22
|
+
CodespellParser,
|
|
23
|
+
GitleaksParser,
|
|
24
|
+
HadolintParser,
|
|
25
|
+
MarkdownlintParser,
|
|
26
|
+
MutmutParser,
|
|
27
|
+
SourceryParser,
|
|
28
|
+
YamllintParser,
|
|
29
|
+
)
|
|
30
|
+
from pyossmtool.parsers.ruff import RuffFormatParser, RuffJsonParser
|
|
31
|
+
from pyossmtool.parsers.shell import ShellcheckParser, ShfmtDiffParser
|
|
32
|
+
|
|
33
|
+
# Keep registered classes referenced for static dead-code analysis.
|
|
34
|
+
_PARSER_CLASSES = (
|
|
35
|
+
BanditParser,
|
|
36
|
+
CliTextParser,
|
|
37
|
+
CodespellParser,
|
|
38
|
+
DeadcodeParser,
|
|
39
|
+
GateJsonParser,
|
|
40
|
+
GitleaksParser,
|
|
41
|
+
HadolintParser,
|
|
42
|
+
JscpdParser,
|
|
43
|
+
MarkdownlintParser,
|
|
44
|
+
MdformatParser,
|
|
45
|
+
MutmutParser,
|
|
46
|
+
NoopParser,
|
|
47
|
+
PydepsCyclesParser,
|
|
48
|
+
PytestParser,
|
|
49
|
+
RadonCcParser,
|
|
50
|
+
RadonMiParser,
|
|
51
|
+
RuffFormatParser,
|
|
52
|
+
RuffJsonParser,
|
|
53
|
+
ScriptTextParser,
|
|
54
|
+
SemgrepParser,
|
|
55
|
+
ShellcheckParser,
|
|
56
|
+
ShfmtDiffParser,
|
|
57
|
+
SourceryParser,
|
|
58
|
+
TyParser,
|
|
59
|
+
VultureParser,
|
|
60
|
+
YamllintParser,
|
|
61
|
+
YamlfmtParser,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_output(check: CheckDef, stdout: str, stderr: str) -> list[Finding]:
|
|
66
|
+
try:
|
|
67
|
+
parser_cls = REGISTRY[check.parser]
|
|
68
|
+
except KeyError as exc:
|
|
69
|
+
raise ValueError(f"Unknown parser: {check.parser}") from exc
|
|
70
|
+
parser = parser_cls()
|
|
71
|
+
if parser_cls.needs_check:
|
|
72
|
+
return parser.parse(stdout, stderr, check=check)
|
|
73
|
+
return parser.parse(stdout, stderr)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
__all__ = ["REGISTRY", "parse_output", "_PARSER_CLASSES"]
|