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 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
@@ -0,0 +1,5 @@
1
+ """CLI interface for OSPAC."""
2
+
3
+ from ospac.cli.commands import cli
4
+
5
+ __all__ = ["cli"]
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()