ospac 0.1.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.
Potentially problematic release.
This version of ospac might be problematic. Click here for more details.
- ospac/__init__.py +19 -0
- ospac/cli/__init__.py +5 -0
- ospac/cli/commands.py +554 -0
- ospac/core/compatibility_matrix.py +332 -0
- ospac/models/__init__.py +12 -0
- ospac/models/compliance.py +161 -0
- ospac/models/license.py +82 -0
- ospac/models/policy.py +97 -0
- ospac/pipeline/__init__.py +14 -0
- ospac/pipeline/data_generator.py +530 -0
- ospac/pipeline/llm_analyzer.py +338 -0
- ospac/pipeline/llm_providers.py +463 -0
- ospac/pipeline/spdx_processor.py +283 -0
- ospac/runtime/__init__.py +11 -0
- ospac/runtime/engine.py +127 -0
- ospac/runtime/evaluator.py +72 -0
- ospac/runtime/loader.py +54 -0
- ospac/utils/__init__.py +3 -0
- ospac-0.1.0.dist-info/METADATA +269 -0
- ospac-0.1.0.dist-info/RECORD +25 -0
- ospac-0.1.0.dist-info/WHEEL +5 -0
- ospac-0.1.0.dist-info/entry_points.txt +2 -0
- ospac-0.1.0.dist-info/licenses/AUTHORS.md +9 -0
- ospac-0.1.0.dist-info/licenses/LICENSE +201 -0
- ospac-0.1.0.dist-info/top_level.txt +1 -0
ospac/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OSPAC - Open Source Policy as Code
|
|
3
|
+
|
|
4
|
+
A comprehensive policy engine for automated OSS license compliance.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
from ospac.runtime.engine import PolicyRuntime
|
|
10
|
+
from ospac.models.license import License
|
|
11
|
+
from ospac.models.policy import Policy
|
|
12
|
+
from ospac.models.compliance import ComplianceResult
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"PolicyRuntime",
|
|
16
|
+
"License",
|
|
17
|
+
"Policy",
|
|
18
|
+
"ComplianceResult",
|
|
19
|
+
]
|
ospac/cli/__init__.py
ADDED
ospac/cli/commands.py
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OSPAC CLI commands.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional, List
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
import yaml
|
|
12
|
+
from colorama import Fore, Style, init
|
|
13
|
+
|
|
14
|
+
from ospac.runtime.engine import PolicyRuntime
|
|
15
|
+
from ospac.models.compliance import ComplianceStatus
|
|
16
|
+
from ospac.pipeline.spdx_processor import SPDXProcessor
|
|
17
|
+
from ospac.pipeline.data_generator import PolicyDataGenerator
|
|
18
|
+
|
|
19
|
+
# Initialize colorama
|
|
20
|
+
init(autoreset=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.group()
|
|
24
|
+
@click.version_option(prog_name="ospac")
|
|
25
|
+
def cli():
|
|
26
|
+
"""OSPAC - Open Source Policy as Code compliance engine."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@cli.command()
|
|
31
|
+
@click.option("--policy-dir", "-p", type=click.Path(exists=True),
|
|
32
|
+
default="policies", help="Path to policy directory")
|
|
33
|
+
@click.option("--licenses", "-l", required=True,
|
|
34
|
+
help="Comma-separated list of licenses to evaluate")
|
|
35
|
+
@click.option("--context", "-c", default="general",
|
|
36
|
+
help="Evaluation context (e.g., static_linking, dynamic_linking)")
|
|
37
|
+
@click.option("--distribution", "-d", default="internal",
|
|
38
|
+
help="Distribution type (internal, commercial, open_source)")
|
|
39
|
+
@click.option("--output", "-o", type=click.Choice(["json", "text", "markdown"]),
|
|
40
|
+
default="text", help="Output format")
|
|
41
|
+
def evaluate(policy_dir: str, licenses: str, context: str,
|
|
42
|
+
distribution: str, output: str):
|
|
43
|
+
"""Evaluate licenses against policies."""
|
|
44
|
+
try:
|
|
45
|
+
runtime = PolicyRuntime.from_path(policy_dir)
|
|
46
|
+
|
|
47
|
+
license_list = [l.strip() for l in licenses.split(",")]
|
|
48
|
+
|
|
49
|
+
eval_context = {
|
|
50
|
+
"licenses_found": license_list,
|
|
51
|
+
"context": context,
|
|
52
|
+
"distribution": distribution,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
result = runtime.evaluate(eval_context)
|
|
56
|
+
|
|
57
|
+
if output == "json":
|
|
58
|
+
click.echo(json.dumps(result.__dict__, indent=2))
|
|
59
|
+
elif output == "markdown":
|
|
60
|
+
_output_markdown(result, license_list)
|
|
61
|
+
else:
|
|
62
|
+
_output_text(result, license_list)
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@cli.command()
|
|
70
|
+
@click.argument("license1")
|
|
71
|
+
@click.argument("license2")
|
|
72
|
+
@click.option("--context", "-c", default="general",
|
|
73
|
+
help="Compatibility context (e.g., static_linking)")
|
|
74
|
+
@click.option("--policy-dir", "-p", type=click.Path(exists=True),
|
|
75
|
+
default="policies", help="Path to policy directory")
|
|
76
|
+
def check(license1: str, license2: str, context: str, policy_dir: str):
|
|
77
|
+
"""Check compatibility between two licenses."""
|
|
78
|
+
try:
|
|
79
|
+
runtime = PolicyRuntime.from_path(policy_dir)
|
|
80
|
+
result = runtime.check_compatibility(license1, license2, context)
|
|
81
|
+
|
|
82
|
+
if result.is_compliant:
|
|
83
|
+
click.secho(f"✓ {license1} and {license2} are compatible", fg="green")
|
|
84
|
+
else:
|
|
85
|
+
click.secho(f"✗ {license1} and {license2} are incompatible", fg="red")
|
|
86
|
+
|
|
87
|
+
if result.violations:
|
|
88
|
+
click.echo("\nViolations:")
|
|
89
|
+
for violation in result.violations:
|
|
90
|
+
click.echo(f" - {violation['message']}")
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@cli.command()
|
|
98
|
+
@click.option("--licenses", "-l", required=True,
|
|
99
|
+
help="Comma-separated list of licenses")
|
|
100
|
+
@click.option("--policy-dir", "-p", type=click.Path(exists=True),
|
|
101
|
+
default="policies", help="Path to policy directory")
|
|
102
|
+
@click.option("--format", "-f", type=click.Choice(["text", "checklist", "markdown"]),
|
|
103
|
+
default="text", help="Output format")
|
|
104
|
+
def obligations(licenses: str, policy_dir: str, format: str):
|
|
105
|
+
"""Get obligations for the specified licenses."""
|
|
106
|
+
try:
|
|
107
|
+
runtime = PolicyRuntime.from_path(policy_dir)
|
|
108
|
+
license_list = [l.strip() for l in licenses.split(",")]
|
|
109
|
+
|
|
110
|
+
obligations_dict = runtime.get_obligations(license_list)
|
|
111
|
+
|
|
112
|
+
if format == "checklist":
|
|
113
|
+
_output_checklist(obligations_dict)
|
|
114
|
+
elif format == "markdown":
|
|
115
|
+
_output_obligations_markdown(obligations_dict)
|
|
116
|
+
else:
|
|
117
|
+
_output_obligations_text(obligations_dict)
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@cli.command()
|
|
125
|
+
@click.argument("policy_file", type=click.Path(exists=True))
|
|
126
|
+
def validate(policy_file: str):
|
|
127
|
+
"""Validate a policy file syntax."""
|
|
128
|
+
try:
|
|
129
|
+
path = Path(policy_file)
|
|
130
|
+
|
|
131
|
+
with open(path, "r") as f:
|
|
132
|
+
if path.suffix == ".json":
|
|
133
|
+
data = json.load(f)
|
|
134
|
+
else:
|
|
135
|
+
data = yaml.safe_load(f)
|
|
136
|
+
|
|
137
|
+
# Basic validation
|
|
138
|
+
if "version" not in data:
|
|
139
|
+
click.secho("⚠ Missing 'version' field", fg="yellow")
|
|
140
|
+
|
|
141
|
+
if "rules" not in data and "license" not in data:
|
|
142
|
+
click.secho("⚠ Missing 'rules' or 'license' field", fg="yellow")
|
|
143
|
+
|
|
144
|
+
click.secho(f"✓ Policy file is valid", fg="green")
|
|
145
|
+
|
|
146
|
+
except (json.JSONDecodeError, yaml.YAMLError) as e:
|
|
147
|
+
click.secho(f"✗ Invalid syntax: {e}", fg="red", err=True)
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@cli.group()
|
|
155
|
+
def data():
|
|
156
|
+
"""Manage OSPAC license data generation."""
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@cli.command()
|
|
161
|
+
@click.argument("license1")
|
|
162
|
+
@click.argument("license2")
|
|
163
|
+
@click.option("--data-dir", "-d", type=click.Path(exists=True),
|
|
164
|
+
default="data/compatibility", help="Path to compatibility data")
|
|
165
|
+
def check_compat(license1: str, license2: str, data_dir: str):
|
|
166
|
+
"""Check compatibility between two licenses using split matrix format."""
|
|
167
|
+
from ospac.core.compatibility_matrix import CompatibilityMatrix
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
# Load the split matrix
|
|
171
|
+
matrix = CompatibilityMatrix(data_dir)
|
|
172
|
+
matrix.load()
|
|
173
|
+
|
|
174
|
+
# Get compatibility status
|
|
175
|
+
status = matrix.get_compatibility(license1, license2)
|
|
176
|
+
|
|
177
|
+
# Display result
|
|
178
|
+
if status == "compatible":
|
|
179
|
+
click.secho(f"✓ {license1} and {license2} are compatible", fg="green")
|
|
180
|
+
elif status == "incompatible":
|
|
181
|
+
click.secho(f"✗ {license1} and {license2} are incompatible", fg="red")
|
|
182
|
+
elif status == "review_needed":
|
|
183
|
+
click.secho(f"⚠ {license1} and {license2} require review", fg="yellow")
|
|
184
|
+
else:
|
|
185
|
+
click.secho(f"? {license1} and {license2} have unknown compatibility", fg="white")
|
|
186
|
+
|
|
187
|
+
# Show compatible licenses
|
|
188
|
+
click.echo(f"\n{license1} is compatible with:")
|
|
189
|
+
compatible = matrix.get_compatible_licenses(license1)[:10] # Show first 10
|
|
190
|
+
for lic in compatible:
|
|
191
|
+
click.echo(f" • {lic}")
|
|
192
|
+
if len(compatible) > 10:
|
|
193
|
+
click.echo(f" ... and {len(compatible) - 10} more")
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
197
|
+
sys.exit(1)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@data.command()
|
|
201
|
+
@click.option("--output-dir", "-o", type=click.Path(), default="data",
|
|
202
|
+
help="Output directory for generated data")
|
|
203
|
+
@click.option("--force", "-f", is_flag=True,
|
|
204
|
+
help="Force re-download of SPDX data")
|
|
205
|
+
@click.option("--force-reprocess", is_flag=True,
|
|
206
|
+
help="Force reprocessing of all licenses (ignore existing)")
|
|
207
|
+
@click.option("--limit", "-l", type=int,
|
|
208
|
+
help="Limit number of licenses to process (for testing)")
|
|
209
|
+
@click.option("--use-llm", is_flag=True, default=False,
|
|
210
|
+
help="Use LLM for enhanced analysis")
|
|
211
|
+
@click.option("--llm-provider", type=click.Choice(["openai", "claude", "ollama"]),
|
|
212
|
+
default="ollama", help="LLM provider to use")
|
|
213
|
+
@click.option("--llm-model", type=str,
|
|
214
|
+
help="LLM model name (auto-selected if not provided)")
|
|
215
|
+
@click.option("--llm-api-key", type=str,
|
|
216
|
+
help="API key for cloud LLM providers (or set OPENAI_API_KEY/ANTHROPIC_API_KEY)")
|
|
217
|
+
def generate(output_dir: str, force: bool, force_reprocess: bool, limit: Optional[int],
|
|
218
|
+
use_llm: bool, llm_provider: str, llm_model: Optional[str], llm_api_key: Optional[str]):
|
|
219
|
+
"""Generate policy data from SPDX licenses."""
|
|
220
|
+
import asyncio
|
|
221
|
+
|
|
222
|
+
async def run_generation():
|
|
223
|
+
# Create generator with LLM configuration
|
|
224
|
+
if use_llm:
|
|
225
|
+
generator = PolicyDataGenerator(
|
|
226
|
+
output_dir=Path(output_dir),
|
|
227
|
+
llm_provider=llm_provider,
|
|
228
|
+
llm_model=llm_model,
|
|
229
|
+
llm_api_key=llm_api_key
|
|
230
|
+
)
|
|
231
|
+
click.echo(f"Using {llm_provider.upper()} LLM provider for enhanced analysis")
|
|
232
|
+
else:
|
|
233
|
+
generator = PolicyDataGenerator(Path(output_dir))
|
|
234
|
+
click.secho("⚠ Running without LLM analysis. Data will be basic.", fg="yellow")
|
|
235
|
+
click.echo("To enable LLM analysis, use --use-llm flag with --llm-provider")
|
|
236
|
+
|
|
237
|
+
click.echo(f"Generating policy data in {output_dir}...")
|
|
238
|
+
|
|
239
|
+
with click.progressbar(length=100, label="Generating data") as bar:
|
|
240
|
+
# This is simplified - in reality would update progress
|
|
241
|
+
summary = await generator.generate_all_data(
|
|
242
|
+
force_download=force,
|
|
243
|
+
limit=limit,
|
|
244
|
+
force_reprocess=force_reprocess
|
|
245
|
+
)
|
|
246
|
+
bar.update(100)
|
|
247
|
+
|
|
248
|
+
click.secho(f"✓ Generated data for {summary['total_licenses']} licenses", fg="green")
|
|
249
|
+
click.echo(f"Output directory: {summary['output_directory']}")
|
|
250
|
+
|
|
251
|
+
# Show category breakdown
|
|
252
|
+
click.echo("\nLicense categories:")
|
|
253
|
+
for category, count in summary.get("categories", {}).items():
|
|
254
|
+
click.echo(f" {category}: {count}")
|
|
255
|
+
|
|
256
|
+
# Show validation results
|
|
257
|
+
validation = summary.get("validation", {})
|
|
258
|
+
if validation.get("is_valid"):
|
|
259
|
+
click.secho("✓ All data validated successfully", fg="green")
|
|
260
|
+
else:
|
|
261
|
+
click.secho(f"⚠ Validation issues found: {len(validation.get('validation_errors', []))}", fg="yellow")
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
asyncio.run(run_generation())
|
|
265
|
+
except Exception as e:
|
|
266
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
267
|
+
sys.exit(1)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@data.command()
|
|
271
|
+
@click.option("--output-dir", "-o", type=click.Path(), default="data",
|
|
272
|
+
help="Output directory for SPDX data")
|
|
273
|
+
@click.option("--force", "-f", is_flag=True,
|
|
274
|
+
help="Force re-download even if cached")
|
|
275
|
+
def download_spdx(output_dir: str, force: bool):
|
|
276
|
+
"""Download SPDX license dataset."""
|
|
277
|
+
try:
|
|
278
|
+
processor = SPDXProcessor(cache_dir=Path(output_dir) / ".cache")
|
|
279
|
+
|
|
280
|
+
click.echo("Downloading SPDX license data...")
|
|
281
|
+
data = processor.download_spdx_data(force=force)
|
|
282
|
+
|
|
283
|
+
click.secho(f"✓ Downloaded {len(data['licenses'])} licenses", fg="green")
|
|
284
|
+
click.echo(f"SPDX version: {data.get('version')}")
|
|
285
|
+
click.echo(f"Release date: {data.get('release_date')}")
|
|
286
|
+
|
|
287
|
+
# Process and save
|
|
288
|
+
click.echo("\nProcessing licenses...")
|
|
289
|
+
processed = processor.process_all_licenses()
|
|
290
|
+
processor.save_processed_data(processed, Path(output_dir))
|
|
291
|
+
|
|
292
|
+
click.secho(f"✓ Processed and saved {len(processed)} licenses", fg="green")
|
|
293
|
+
|
|
294
|
+
except Exception as e:
|
|
295
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
296
|
+
sys.exit(1)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@data.command()
|
|
300
|
+
@click.argument("license_id")
|
|
301
|
+
@click.option("--format", "-f", type=click.Choice(["json", "yaml", "text"]),
|
|
302
|
+
default="yaml", help="Output format")
|
|
303
|
+
def show(license_id: str, format: str):
|
|
304
|
+
"""Show details for a specific license from the database."""
|
|
305
|
+
try:
|
|
306
|
+
# Try to load from generated data
|
|
307
|
+
data_file = Path("data") / "ospac_license_database.json"
|
|
308
|
+
|
|
309
|
+
if not data_file.exists():
|
|
310
|
+
click.secho("No generated data found. Run 'ospac data generate' first.", fg="red")
|
|
311
|
+
sys.exit(1)
|
|
312
|
+
|
|
313
|
+
with open(data_file) as f:
|
|
314
|
+
database = json.load(f)
|
|
315
|
+
|
|
316
|
+
if license_id not in database.get("licenses", {}):
|
|
317
|
+
click.secho(f"License {license_id} not found in database", fg="red")
|
|
318
|
+
click.echo("\nAvailable licenses (first 10):")
|
|
319
|
+
for lid in list(database.get("licenses", {}).keys())[:10]:
|
|
320
|
+
click.echo(f" - {lid}")
|
|
321
|
+
sys.exit(1)
|
|
322
|
+
|
|
323
|
+
license_data = database["licenses"][license_id]
|
|
324
|
+
|
|
325
|
+
if format == "json":
|
|
326
|
+
click.echo(json.dumps(license_data, indent=2))
|
|
327
|
+
elif format == "yaml":
|
|
328
|
+
click.echo(yaml.dump(license_data, default_flow_style=False))
|
|
329
|
+
else:
|
|
330
|
+
# Text format
|
|
331
|
+
click.secho(f"License: {license_id}", fg="cyan", bold=True)
|
|
332
|
+
click.echo(f"Category: {license_data.get('category')}")
|
|
333
|
+
click.echo(f"Name: {license_data.get('name')}")
|
|
334
|
+
|
|
335
|
+
click.echo("\nPermissions:")
|
|
336
|
+
for perm, value in license_data.get("permissions", {}).items():
|
|
337
|
+
symbol = "✓" if value else "✗"
|
|
338
|
+
click.echo(f" {symbol} {perm}")
|
|
339
|
+
|
|
340
|
+
click.echo("\nConditions:")
|
|
341
|
+
for cond, value in license_data.get("conditions", {}).items():
|
|
342
|
+
if value:
|
|
343
|
+
click.echo(f" • {cond}")
|
|
344
|
+
|
|
345
|
+
click.echo("\nObligations:")
|
|
346
|
+
for obligation in license_data.get("obligations", []):
|
|
347
|
+
click.echo(f" • {obligation}")
|
|
348
|
+
|
|
349
|
+
except Exception as e:
|
|
350
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
351
|
+
sys.exit(1)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@data.command()
|
|
355
|
+
@click.option("--data-dir", "-d", type=click.Path(exists=True),
|
|
356
|
+
default="data", help="Directory containing generated data")
|
|
357
|
+
def validate(data_dir: str):
|
|
358
|
+
"""Validate generated policy data."""
|
|
359
|
+
try:
|
|
360
|
+
data_path = Path(data_dir)
|
|
361
|
+
|
|
362
|
+
# Check required files
|
|
363
|
+
required_files = [
|
|
364
|
+
"ospac_license_database.json",
|
|
365
|
+
"compatibility_matrix.json",
|
|
366
|
+
"obligation_database.json",
|
|
367
|
+
"generation_summary.json"
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
missing = []
|
|
371
|
+
for file_name in required_files:
|
|
372
|
+
if not (data_path / file_name).exists():
|
|
373
|
+
missing.append(file_name)
|
|
374
|
+
|
|
375
|
+
if missing:
|
|
376
|
+
click.secho(f"✗ Missing required files:", fg="red")
|
|
377
|
+
for file_name in missing:
|
|
378
|
+
click.echo(f" - {file_name}")
|
|
379
|
+
sys.exit(1)
|
|
380
|
+
|
|
381
|
+
# Load and validate master database
|
|
382
|
+
with open(data_path / "ospac_license_database.json") as f:
|
|
383
|
+
database = json.load(f)
|
|
384
|
+
|
|
385
|
+
total = len(database.get("licenses", {}))
|
|
386
|
+
click.echo(f"Validating {total} licenses...")
|
|
387
|
+
|
|
388
|
+
issues = []
|
|
389
|
+
for license_id, data in database.get("licenses", {}).items():
|
|
390
|
+
if not data.get("category"):
|
|
391
|
+
issues.append(f"{license_id}: Missing category")
|
|
392
|
+
if not data.get("permissions"):
|
|
393
|
+
issues.append(f"{license_id}: Missing permissions")
|
|
394
|
+
|
|
395
|
+
if issues:
|
|
396
|
+
click.secho(f"⚠ Found {len(issues)} validation issues:", fg="yellow")
|
|
397
|
+
for issue in issues[:10]: # Show first 10
|
|
398
|
+
click.echo(f" - {issue}")
|
|
399
|
+
else:
|
|
400
|
+
click.secho(f"✓ All {total} licenses validated successfully", fg="green")
|
|
401
|
+
|
|
402
|
+
# Show summary
|
|
403
|
+
with open(data_path / "generation_summary.json") as f:
|
|
404
|
+
summary = json.load(f)
|
|
405
|
+
|
|
406
|
+
click.echo(f"\nGeneration summary:")
|
|
407
|
+
click.echo(f" Generated: {summary.get('generated_at')}")
|
|
408
|
+
click.echo(f" SPDX version: {summary.get('spdx_version')}")
|
|
409
|
+
|
|
410
|
+
click.echo("\nCategories:")
|
|
411
|
+
for cat, count in summary.get("categories", {}).items():
|
|
412
|
+
click.echo(f" {cat}: {count}")
|
|
413
|
+
|
|
414
|
+
except Exception as e:
|
|
415
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
416
|
+
sys.exit(1)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@cli.command()
|
|
420
|
+
@click.option("--template", "-t",
|
|
421
|
+
type=click.Choice(["enterprise", "startup", "opensource", "permissive", "strict"]),
|
|
422
|
+
default="enterprise", help="Policy template to use")
|
|
423
|
+
@click.option("--output", "-o", type=click.Path(),
|
|
424
|
+
default="my_policy.yaml", help="Output file path")
|
|
425
|
+
def init(template: str, output: str):
|
|
426
|
+
"""Initialize a new policy from a template."""
|
|
427
|
+
templates = {
|
|
428
|
+
"enterprise": {
|
|
429
|
+
"version": "1.0",
|
|
430
|
+
"name": "Enterprise Policy",
|
|
431
|
+
"rules": [
|
|
432
|
+
{
|
|
433
|
+
"id": "no_copyleft",
|
|
434
|
+
"description": "Prevent copyleft in products",
|
|
435
|
+
"when": {"license_type": "copyleft_strong"},
|
|
436
|
+
"then": {
|
|
437
|
+
"action": "deny",
|
|
438
|
+
"severity": "error",
|
|
439
|
+
"message": "Strong copyleft licenses not allowed",
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
]
|
|
443
|
+
},
|
|
444
|
+
"permissive": {
|
|
445
|
+
"version": "1.0",
|
|
446
|
+
"name": "Permissive Policy",
|
|
447
|
+
"rules": [
|
|
448
|
+
{
|
|
449
|
+
"id": "prefer_permissive",
|
|
450
|
+
"description": "Allow only permissive licenses",
|
|
451
|
+
"when": {"license_type": ["permissive", "public_domain"]},
|
|
452
|
+
"then": {"action": "allow"}
|
|
453
|
+
}
|
|
454
|
+
]
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
policy = templates.get(template, templates["enterprise"])
|
|
459
|
+
|
|
460
|
+
with open(output, "w") as f:
|
|
461
|
+
yaml.dump(policy, f, default_flow_style=False)
|
|
462
|
+
|
|
463
|
+
click.secho(f"✓ Created policy file: {output}", fg="green")
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _output_text(result, licenses):
|
|
467
|
+
"""Output result in text format."""
|
|
468
|
+
click.echo(f"\nEvaluating licenses: {', '.join(licenses)}")
|
|
469
|
+
click.echo("-" * 50)
|
|
470
|
+
|
|
471
|
+
if hasattr(result, "action"):
|
|
472
|
+
action_color = "green" if result.action.value == "allow" else "red"
|
|
473
|
+
click.secho(f"Action: {result.action.value}", fg=action_color)
|
|
474
|
+
|
|
475
|
+
if result.message:
|
|
476
|
+
click.echo(f"Message: {result.message}")
|
|
477
|
+
|
|
478
|
+
if result.requirements:
|
|
479
|
+
click.echo("\nRequirements:")
|
|
480
|
+
for req in result.requirements:
|
|
481
|
+
click.echo(f" • {req}")
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _output_markdown(result, licenses):
|
|
485
|
+
"""Output result in markdown format."""
|
|
486
|
+
click.echo(f"# License Evaluation Report\n")
|
|
487
|
+
click.echo(f"**Licenses evaluated:** {', '.join(licenses)}\n")
|
|
488
|
+
|
|
489
|
+
if hasattr(result, "action"):
|
|
490
|
+
status = "✅ Allowed" if result.action.value == "allow" else "❌ Denied"
|
|
491
|
+
click.echo(f"## Status: {status}\n")
|
|
492
|
+
|
|
493
|
+
if result.message:
|
|
494
|
+
click.echo(f"**Message:** {result.message}\n")
|
|
495
|
+
|
|
496
|
+
if result.requirements:
|
|
497
|
+
click.echo("## Requirements\n")
|
|
498
|
+
for req in result.requirements:
|
|
499
|
+
click.echo(f"- {req}")
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _output_checklist(obligations_dict):
|
|
503
|
+
"""Output obligations as a checklist."""
|
|
504
|
+
for license_id, oblig in obligations_dict.items():
|
|
505
|
+
click.echo(f"\n{license_id}:")
|
|
506
|
+
click.echo("-" * 40)
|
|
507
|
+
|
|
508
|
+
if isinstance(oblig, dict):
|
|
509
|
+
for key, value in oblig.items():
|
|
510
|
+
if isinstance(value, bool):
|
|
511
|
+
checkbox = "☑" if value else "☐"
|
|
512
|
+
click.echo(f" {checkbox} {key}")
|
|
513
|
+
elif isinstance(value, list):
|
|
514
|
+
for item in value:
|
|
515
|
+
click.echo(f" ☐ {item}")
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _output_obligations_text(obligations_dict):
|
|
519
|
+
"""Output obligations in text format."""
|
|
520
|
+
for license_id, oblig in obligations_dict.items():
|
|
521
|
+
click.secho(f"\n{license_id}:", fg="cyan", bold=True)
|
|
522
|
+
|
|
523
|
+
if isinstance(oblig, dict):
|
|
524
|
+
for key, value in oblig.items():
|
|
525
|
+
if value:
|
|
526
|
+
click.echo(f" • {key}: {value}")
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _output_obligations_markdown(obligations_dict):
|
|
530
|
+
"""Output obligations in markdown format."""
|
|
531
|
+
click.echo("# License Obligations\n")
|
|
532
|
+
|
|
533
|
+
for license_id, oblig in obligations_dict.items():
|
|
534
|
+
click.echo(f"## {license_id}\n")
|
|
535
|
+
|
|
536
|
+
if isinstance(oblig, dict):
|
|
537
|
+
for key, value in oblig.items():
|
|
538
|
+
if isinstance(value, bool) and value:
|
|
539
|
+
click.echo(f"- **{key}**")
|
|
540
|
+
elif isinstance(value, str):
|
|
541
|
+
click.echo(f"- **{key}:** {value}")
|
|
542
|
+
elif isinstance(value, list):
|
|
543
|
+
click.echo(f"- **{key}:**")
|
|
544
|
+
for item in value:
|
|
545
|
+
click.echo(f" - {item}")
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def main():
|
|
549
|
+
"""Main entry point."""
|
|
550
|
+
cli()
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
if __name__ == "__main__":
|
|
554
|
+
main()
|