| 1 | from datetime import datetime |
| 2 | from typing import Dict |
| 3 | from typing import List |
| 4 | from typing import Optional |
| 5 | |
| 6 | from sqlmodel import JSON |
| 7 | from sqlmodel import Column |
| 8 | from sqlmodel import Field |
| 9 | from sqlmodel import Relationship |
| 10 | from sqlmodel import SQLModel |
| 11 | from sqlmodel import Text |
| 12 | |
| 13 | |
| 14 | class GitHubAuditConfig(SQLModel, table=True): |
| 15 | """Configuration for GitHub organization security audits per customer.""" |
| 16 | |
| 17 | __tablename__ = "github_audit_config" |
| 18 | |
| 19 | id: Optional[int] = Field(default=None, primary_key=True) |
| 20 | customer_code: str = Field(max_length=50, nullable=False, index=True) |
| 21 | |
| 22 | # GitHub Authentication |
| 23 | github_token: str = Field(max_length=500, nullable=False, description="GitHub PAT or App token (encrypted)") |
| 24 | organization: str = Field(max_length=100, nullable=False, description="GitHub organization name") |
| 25 | |
| 26 | # Token metadata |
| 27 | token_type: str = Field( |
| 28 | max_length=50, |
| 29 | default="pat", |
| 30 | description="Token type: 'pat' (Personal Access Token) or 'app' (GitHub App)", |
| 31 | ) |
| 32 | token_expires_at: Optional[datetime] = Field( |
| 33 | nullable=True, |
| 34 | description="When the token expires (if applicable)", |
| 35 | ) |
| 36 | |
| 37 | # Audit configuration |
| 38 | enabled: bool = Field(default=True, description="Whether audits are enabled for this org") |
| 39 | auto_audit_enabled: bool = Field( |
| 40 | default=False, |
| 41 | description="Whether to run audits automatically on schedule", |
| 42 | ) |
| 43 | audit_schedule_cron: Optional[str] = Field( |
| 44 | max_length=50, |
| 45 | nullable=True, |
| 46 | description="Cron expression for scheduled audits (e.g., '0 0 * * 1' for weekly Monday)", |
| 47 | ) |
| 48 | |
| 49 | # Scope options - what to include in audits |
| 50 | include_repos: bool = Field(default=True, description="Include repository-level audits") |
| 51 | include_workflows: bool = Field(default=True, description="Include GitHub Actions audits") |
| 52 | include_members: bool = Field(default=True, description="Include member/permission audits") |
| 53 | include_archived_repos: bool = Field(default=False, description="Include archived repositories") |
| 54 | |
| 55 | # Filtering |
| 56 | repo_filter_mode: str = Field( |
| 57 | max_length=20, |
| 58 | default="all", |
| 59 | description="'all', 'include', or 'exclude'", |
| 60 | ) |
| 61 | repo_filter_list: Optional[List[str]] = Field( |
| 62 | sa_column=Column(JSON, nullable=True), |
| 63 | description="List of repos to include/exclude based on filter_mode", |
| 64 | ) |
| 65 | |
| 66 | # Notification settings |
| 67 | notify_on_critical: bool = Field(default=True, description="Send notification on critical findings") |
| 68 | notify_on_high: bool = Field(default=False, description="Send notification on high findings") |
| 69 | notification_webhook_url: Optional[str] = Field( |
| 70 | max_length=500, |
| 71 | nullable=True, |
| 72 | description="Webhook URL for audit notifications", |
| 73 | ) |
| 74 | notification_email: Optional[str] = Field( |
| 75 | max_length=255, |
| 76 | nullable=True, |
| 77 | description="Email for audit notifications", |
| 78 | ) |
| 79 | |
| 80 | # Thresholds |
| 81 | minimum_passing_score: float = Field( |
| 82 | default=70.0, |
| 83 | description="Minimum score to consider audit passing (0-100)", |
| 84 | ) |
| 85 | |
| 86 | # Metadata |
| 87 | created_at: datetime = Field(default_factory=datetime.utcnow) |
| 88 | updated_at: datetime = Field(default_factory=datetime.utcnow) |
| 89 | created_by: Optional[str] = Field(max_length=100, nullable=True) |
| 90 | updated_by: Optional[str] = Field(max_length=100, nullable=True) |
| 91 | |
| 92 | # Last audit info |
| 93 | last_audit_at: Optional[datetime] = Field(nullable=True, description="When last audit was run") |
| 94 | last_audit_score: Optional[float] = Field(nullable=True, description="Score from last audit") |
| 95 | last_audit_grade: Optional[str] = Field(max_length=2, nullable=True, description="Grade from last audit") |
| 96 | |
| 97 | # Relationship to audit reports |
| 98 | audit_reports: List["GitHubAuditReport"] = Relationship(back_populates="config") |
| 99 | |
| 100 | |
| 101 | class GitHubAuditReport(SQLModel, table=True): |
| 102 | """Stored GitHub audit reports.""" |
| 103 | |
| 104 | __tablename__ = "github_audit_report" |
| 105 | |
| 106 | id: Optional[int] = Field(default=None, primary_key=True) |
| 107 | config_id: int = Field(foreign_key="github_audit_config.id", nullable=False, index=True) |
| 108 | customer_code: str = Field(max_length=50, nullable=False, index=True) |
| 109 | |
| 110 | # Report identification |
| 111 | report_name: str = Field(max_length=255, nullable=False) |
| 112 | organization: str = Field(max_length=100, nullable=False) |
| 113 | |
| 114 | # Audit timing |
| 115 | audit_started_at: datetime = Field(default_factory=datetime.utcnow) |
| 116 | audit_completed_at: Optional[datetime] = Field(nullable=True) |
| 117 | audit_duration_seconds: Optional[float] = Field(nullable=True) |
| 118 | |
| 119 | # Summary data |
| 120 | total_repos_audited: int = Field(default=0) |
| 121 | total_checks: int = Field(default=0) |
| 122 | passed_checks: int = Field(default=0) |
| 123 | failed_checks: int = Field(default=0) |
| 124 | warning_checks: int = Field(default=0) |
| 125 | critical_findings: int = Field(default=0) |
| 126 | high_findings: int = Field(default=0) |
| 127 | medium_findings: int = Field(default=0) |
| 128 | low_findings: int = Field(default=0) |
| 129 | score: float = Field(default=0.0) |
| 130 | grade: str = Field(max_length=2, default="F") |
| 131 | |
| 132 | # Status |
| 133 | status: str = Field( |
| 134 | max_length=50, |
| 135 | default="running", |
| 136 | description="'running', 'completed', 'failed'", |
| 137 | ) |
| 138 | error_message: Optional[str] = Field(sa_column=Column(Text, nullable=True)) |
| 139 | |
| 140 | # Full report data stored as JSON |
| 141 | full_report: Optional[Dict] = Field(sa_column=Column(JSON, nullable=True), description="Complete audit report data") |
| 142 | |
| 143 | # Top findings for quick access |
| 144 | top_findings: Optional[List[Dict]] = Field(sa_column=Column(JSON, nullable=True), description="Top priority findings") |
| 145 | |
| 146 | # Triggered by |
| 147 | triggered_by: str = Field( |
| 148 | max_length=50, |
| 149 | default="manual", |
| 150 | description="'manual', 'scheduled', 'api'", |
| 151 | ) |
| 152 | triggered_by_user: Optional[str] = Field(max_length=100, nullable=True) |
| 153 | |
| 154 | # Relationship back to config |
| 155 | config: GitHubAuditConfig = Relationship(back_populates="audit_reports") |
| 156 | |
| 157 | |
| 158 | class GitHubAuditCheckExclusion(SQLModel, table=True): |
| 159 | """Exclusion rules for specific audit checks.""" |
| 160 | |
| 161 | __tablename__ = "github_audit_check_exclusion" |
| 162 | |
| 163 | id: Optional[int] = Field(default=None, primary_key=True) |
| 164 | config_id: int = Field(foreign_key="github_audit_config.id", nullable=False, index=True) |
| 165 | customer_code: str = Field(max_length=50, nullable=False, index=True) |
| 166 | |
| 167 | # What to exclude |
| 168 | check_id: str = Field( |
| 169 | max_length=100, |
| 170 | nullable=False, |
| 171 | description="The check ID to exclude (e.g., 'repo-branch-protection')", |
| 172 | ) |
| 173 | resource_name: Optional[str] = Field( |
| 174 | max_length=255, |
| 175 | nullable=True, |
| 176 | description="Specific resource to exclude (e.g., repo name). Null = all resources", |
| 177 | ) |
| 178 | resource_type: Optional[str] = Field( |
| 179 | max_length=50, |
| 180 | nullable=True, |
| 181 | description="Type of resource: 'organization', 'repository', 'workflow', 'member'", |
| 182 | ) |
| 183 | |
| 184 | # Why excluded |
| 185 | reason: str = Field(sa_column=Column(Text, nullable=False), description="Reason for exclusion") |
| 186 | approved_by: Optional[str] = Field(max_length=100, nullable=True) |
| 187 | approved_at: Optional[datetime] = Field(nullable=True) |
| 188 | |
| 189 | # Expiration |
| 190 | expires_at: Optional[datetime] = Field( |
| 191 | nullable=True, |
| 192 | description="When this exclusion expires (null = never)", |
| 193 | ) |
| 194 | |
| 195 | # Metadata |
| 196 | enabled: bool = Field(default=True) |
| 197 | created_at: datetime = Field(default_factory=datetime.utcnow) |
| 198 | created_by: str = Field(max_length=100, nullable=False) |
| 199 | |
| 200 | |
| 201 | class GitHubAuditBaseline(SQLModel, table=True): |
| 202 | """Baseline configuration for expected audit results.""" |
| 203 | |
| 204 | __tablename__ = "github_audit_baseline" |
| 205 | |
| 206 | id: Optional[int] = Field(default=None, primary_key=True) |
| 207 | config_id: int = Field(foreign_key="github_audit_config.id", nullable=False, index=True) |
| 208 | customer_code: str = Field(max_length=50, nullable=False, index=True) |
| 209 | |
| 210 | # Baseline name |
| 211 | name: str = Field(max_length=255, nullable=False) |
| 212 | description: Optional[str] = Field(sa_column=Column(Text, nullable=True)) |
| 213 | |
| 214 | # Expected values |
| 215 | expected_checks: Optional[Dict] = Field( |
| 216 | sa_column=Column(JSON, nullable=True), |
| 217 | description="Expected check results by check_id: {check_id: expected_status}", |
| 218 | ) |
| 219 | |
| 220 | # Baseline from a previous report |
| 221 | baseline_report_id: Optional[int] = Field( |
| 222 | foreign_key="github_audit_report.id", |
| 223 | nullable=True, |
| 224 | description="Report used to create this baseline", |
| 225 | ) |
| 226 | |
| 227 | # Metadata |
| 228 | is_active: bool = Field(default=True, description="Whether this is the active baseline") |
| 229 | created_at: datetime = Field(default_factory=datetime.utcnow) |
| 230 | created_by: str = Field(max_length=100, nullable=False) |