mirror of
https://github.com/ivuorinen/ghaw-auditor.git
synced 2026-01-26 03:14:09 +00:00
255 lines
7.3 KiB
Python
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)
|