Files
ghaw-auditor/ghaw_auditor/models.py
2025-10-19 09:52:13 +03:00

255 lines
7.3 KiB
Python

"""Pydantic models for GitHub Actions and Workflows."""
from __future__ import annotations
from datetime import datetime
from enum import Enum
from typing import Any
from pydantic import BaseModel, Field
class ActionType(str, Enum):
"""Type of action reference."""
LOCAL = "local"
GITHUB = "github"
DOCKER = "docker"
REUSABLE_WORKFLOW = "reusable_workflow"
class ActionRef(BaseModel):
"""Reference to an action with version info."""
type: ActionType
owner: str | None = None
repo: str | None = None
path: str | None = None
ref: str | None = None # Tag, branch, or SHA
resolved_sha: str | None = None
source_file: str
source_line: int | None = None
def canonical_key(self) -> str:
"""Generate unique key for deduplication."""
if self.type == ActionType.LOCAL:
return f"local:{self.path}"
elif self.type == ActionType.DOCKER:
return f"docker:{self.path}"
elif self.type == ActionType.REUSABLE_WORKFLOW:
return f"{self.owner}/{self.repo}/{self.path}@{self.resolved_sha or self.ref}"
return f"{self.owner}/{self.repo}@{self.resolved_sha or self.ref}"
class ActionInput(BaseModel):
"""Action input definition."""
name: str
description: str | None = None
required: bool = False
default: str | bool | int | None = None
class ActionOutput(BaseModel):
"""Action output definition."""
name: str
description: str | None = None
class ActionManifest(BaseModel):
"""Parsed action.yml manifest."""
name: str
description: str | None = None
author: str | None = None
inputs: dict[str, ActionInput] = Field(default_factory=dict)
outputs: dict[str, ActionOutput] = Field(default_factory=dict)
runs: dict[str, Any] = Field(default_factory=dict)
branding: dict[str, str] | None = None
is_composite: bool = False
is_docker: bool = False
is_javascript: bool = False
class PermissionLevel(str, Enum):
"""Permission level."""
NONE = "none"
READ = "read"
WRITE = "write"
class Permissions(BaseModel):
"""Job or workflow permissions."""
actions: PermissionLevel | None = None
checks: PermissionLevel | None = None
contents: PermissionLevel | None = None
deployments: PermissionLevel | None = None
id_token: PermissionLevel | None = None
issues: PermissionLevel | None = None
packages: PermissionLevel | None = None
pages: PermissionLevel | None = None
pull_requests: PermissionLevel | None = None
repository_projects: PermissionLevel | None = None
security_events: PermissionLevel | None = None
statuses: PermissionLevel | None = None
class Container(BaseModel):
"""Container configuration."""
image: str
credentials: dict[str, str] | None = None
env: dict[str, str | int | float | bool] = Field(default_factory=dict)
ports: list[int] = Field(default_factory=list)
volumes: list[str] = Field(default_factory=list)
options: str | None = None
class Service(BaseModel):
"""Service container configuration."""
name: str
image: str
credentials: dict[str, str] | None = None
env: dict[str, str | int | float | bool] = Field(default_factory=dict)
ports: list[int] = Field(default_factory=list)
volumes: list[str] = Field(default_factory=list)
options: str | None = None
class Strategy(BaseModel):
"""Job matrix strategy."""
matrix: dict[str, Any] = Field(default_factory=dict)
fail_fast: bool = True
max_parallel: int | None = None
class JobMeta(BaseModel):
"""Job metadata."""
name: str
runs_on: str | list[str]
needs: list[str] = Field(default_factory=list)
if_condition: str | None = Field(None, alias="if")
permissions: Permissions | None = None
environment: str | dict[str, Any] | None = None
concurrency: str | dict[str, Any] | None = None
timeout_minutes: int | None = None
continue_on_error: bool = False
container: Container | None = None
services: dict[str, Service] = Field(default_factory=dict)
strategy: Strategy | None = None
# Reusable workflow fields
uses: str | None = None # Reusable workflow reference
with_inputs: dict[str, Any] = Field(default_factory=dict) # Inputs via 'with'
secrets_passed: dict[str, str] | None = None # Secrets passed to reusable workflow
inherit_secrets: bool = False # Whether secrets: inherit is used
outputs: dict[str, Any] = Field(default_factory=dict) # Job outputs
# Action tracking
actions_used: list[ActionRef] = Field(default_factory=list)
secrets_used: set[str] = Field(default_factory=set)
env_vars: dict[str, str | int | float | bool] = Field(default_factory=dict)
class ReusableContract(BaseModel):
"""Reusable workflow contract."""
inputs: dict[str, Any] = Field(default_factory=dict)
outputs: dict[str, Any] = Field(default_factory=dict)
secrets: dict[str, Any] = Field(default_factory=dict)
class WorkflowMeta(BaseModel):
"""Workflow metadata."""
name: str
path: str
triggers: list[str] = Field(default_factory=list)
permissions: Permissions | None = None
concurrency: str | dict[str, Any] | None = None
env: dict[str, str | int | float | bool] = Field(default_factory=dict)
defaults: dict[str, Any] = Field(default_factory=dict)
jobs: dict[str, JobMeta] = Field(default_factory=dict)
is_reusable: bool = False
reusable_contract: ReusableContract | None = None
secrets_used: set[str] = Field(default_factory=set)
actions_used: list[ActionRef] = Field(default_factory=list)
class PolicyRule(BaseModel):
"""Policy rule."""
name: str
enabled: bool = True
severity: str = "warning" # warning, error
config: dict[str, Any] = Field(default_factory=dict)
class Policy(BaseModel):
"""Audit policy configuration."""
min_permissions: bool = True
require_pinned_actions: bool = True
forbid_branch_refs: bool = False
allowed_actions: list[str] = Field(default_factory=list)
denied_actions: list[str] = Field(default_factory=list)
require_concurrency_on_pr: bool = False
custom_rules: list[PolicyRule] = Field(default_factory=list)
class BaselineMeta(BaseModel):
"""Baseline metadata."""
auditor_version: str
commit_sha: str | None = None
timestamp: datetime
schema_version: str = "1.0"
class Baseline(BaseModel):
"""Baseline snapshot for diff mode."""
meta: BaselineMeta
actions: dict[str, ActionManifest]
workflows: dict[str, WorkflowMeta]
class DiffEntry(BaseModel):
"""Single diff entry."""
field: str
old_value: Any = None
new_value: Any = None
change_type: str # added, removed, modified
class ActionDiff(BaseModel):
"""Action diff."""
key: str
status: str # added, removed, modified, unchanged
changes: list[DiffEntry] = Field(default_factory=list)
class WorkflowDiff(BaseModel):
"""Workflow diff."""
path: str
status: str # added, removed, modified, unchanged
changes: list[DiffEntry] = Field(default_factory=list)
class AuditReport(BaseModel):
"""Complete audit report."""
generated_at: datetime
repository: str
commit_sha: str | None = None
actions: dict[str, ActionManifest]
workflows: dict[str, WorkflowMeta]
policy_violations: list[dict[str, Any]] = Field(default_factory=list)