This commit is contained in:
Iliyan Angelov
2025-12-01 06:50:10 +02:00
parent 91f51bc6fe
commit 62c1fe5951
4682 changed files with 544807 additions and 31208 deletions

View File

@@ -0,0 +1,27 @@
OS_TYPE_DESC: str = (
"SAFETY_OS_TYPE or platform.system() value, SAFETY_OS_TYPE has "
"higher priority, it can be empty."
)
OS_RELEASE_DESC: str = (
"SAFETY_OS_RELEASE or platform.release() value, "
"SAFETY_OS_RELEASE has higher priority, it can be empty."
)
OS_DESCRIPTION_DESC: str = (
"SAFETY_OS_DESCRIPTION or platform.platform() value, "
"SAFETY_OS_DESCRIPTION has higher priority."
)
PYTHON_VERSION_DESC: str = (
"The output of call platform.python_version(), " "major.minor.patchlevel value."
)
SAFETY_COMMAND_DESC: str = "The command used in Safety CLI."
SAFETY_OPTIONS_DESC: str = (
"Logs the options used and the flag names used and how "
"many times are used. Useful to understand the impact of a deprecation."
)
SAFETY_VERSION_DESC: str = "A PEP 440 valid version of Safety CLI."
SAFETY_SOURCE_DESC: str = "What is the source of the safety use."
BRANCH_DESC: str = (
"The current branch name inside of the directory " "where was found git."
)
TAG_DESC: str = "If the current status is a tag, this represents the tag."

View File

@@ -0,0 +1,423 @@
from __future__ import annotations
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional, Union
# from pydantic import BaseModel, Field, root_validator
from typing_extensions import Annotated, Literal
try:
from pydantic import Field, model_validator # type: ignore # pragma: no cover
from pydantic import BaseModel # type: ignore # pragma: no cover
MODEL_VALIDATOR_KWARGS = {"mode": "after"}
LATEST_PYDANTIC = True
except ImportError:
from pydantic import Field, root_validator as model_validator # type: ignore # noqa F401 # pragma: no cover
from pydantic import BaseModel # type: ignore # pragma: no cover
MODEL_VALIDATOR_KWARGS = {}
LATEST_PYDANTIC = False
from .constants import (
BRANCH_DESC,
OS_DESCRIPTION_DESC,
OS_RELEASE_DESC,
OS_TYPE_DESC,
PYTHON_VERSION_DESC,
SAFETY_COMMAND_DESC,
SAFETY_OPTIONS_DESC,
SAFETY_SOURCE_DESC,
SAFETY_VERSION_DESC,
TAG_DESC,
)
class SchemaModelV30(BaseModel):
def json(self, *args, **kwargs) -> str:
if LATEST_PYDANTIC:
return self.model_dump_json(**kwargs)
return super().json(**kwargs)
@classmethod
def parse_obj(cls: "SchemaModelV30", obj: Any) -> "SchemaModelV30":
if LATEST_PYDANTIC:
return cls.model_validate(obj)
return super(SchemaModelV30, cls).parse_obj(obj)
class Telemetry(SchemaModelV30):
"""
Telemetry object generated per Safety report; this model holds data related to the
client application running Safety CLI.
"""
os_type: Annotated[
Optional[str], Field(description=OS_TYPE_DESC, examples=["Darwin", "Linux", ""])
] = None
os_release: Annotated[
Optional[str],
Field(
description=OS_RELEASE_DESC,
examples=["21.4.0", "NT", ""],
),
] = None
os_description: Annotated[
Optional[str],
Field(
description=OS_DESCRIPTION_DESC,
examples=["macOS-12.3.1-arm64-arm-64bit"],
),
] = None
python_version: Annotated[
Optional[str], Field(description=PYTHON_VERSION_DESC, examples=["3.11.1"])
] = None
safety_command: Annotated[
Optional[str],
Field(
description=SAFETY_COMMAND_DESC,
examples=["check", "configure", "scan"],
),
] = None
safety_options: Annotated[
Dict[str, Dict[str, int]], Field(description=SAFETY_OPTIONS_DESC)
]
safety_version: Annotated[
str,
Field(
...,
description=SAFETY_VERSION_DESC,
examples=["2.4.0b1", "3.0"],
),
]
safety_source: Annotated[
str,
Field(
...,
description=SAFETY_SOURCE_DESC,
examples=["cli", "code"],
),
]
class Git(SchemaModelV30):
branch: Optional[str] = Field(
...,
description=BRANCH_DESC,
examples=["feat/find-cve-package-identifiers", None],
title="Branch",
)
tag: Optional[str] = Field(
...,
description=TAG_DESC,
examples=["2.0.0", None],
title="Tag",
)
commit: Optional[str] = Field(
...,
description="Current git commit of 40 lenght",
examples=[
"514235567bf3194a8ae97f8cd1c7f1011bded271",
"bac8714e03c33e177205fe749e6d994c8ad47634-dirty",
],
title="Commit",
)
dirty: bool = Field(
...,
description="If there are modified tracked files "
"and/or staged changes. Untracked files aren't considered.",
examples=[True],
title="Dirty",
)
origin: Optional[str] = Field(
...,
description="URL for the origin remote if it exists, otherwise null.",
examples=["git@github.com:pyupio/pyupio.git", None],
title="Origin",
)
class AuthenticationMethod(str, Enum):
token = "token"
api_key = "api_key"
class ScanType(str, Enum):
scan = "scan"
system_scan = "system-scan"
check = "check"
class StageType(str, Enum):
development = "development"
cicd = "cicd"
production = "production"
class IgnoredDetails(SchemaModelV30):
code: str
reason: Optional[str]
expires: Optional[str]
class Vulnerability(SchemaModelV30):
id: Annotated[str, Field()]
ignored: Annotated[
Optional[IgnoredDetails],
Field(..., description="Only exported when the vulnerability is " "ignored"),
] = None
vulnerable_spec: Annotated[str, Field()]
def __get_exclude_fields__(self, kwargs):
exclude = kwargs.pop("exclude", set())
if not exclude:
exclude = set()
optional_properties = ["ignored", "analyzed_version"]
for opt_property in optional_properties:
value = None
try:
value = getattr(self, opt_property)
except AttributeError:
pass
if not value:
exclude.add(opt_property)
return exclude
def dict(self, **kwargs):
exclude = self.__get_exclude_fields__(kwargs)
if LATEST_PYDANTIC:
return self.model_dump(exclude=exclude, **kwargs)
return super().dict(exclude=exclude, **kwargs)
def json(self, **kwargs) -> str:
exclude = self.__get_exclude_fields__(kwargs)
if LATEST_PYDANTIC:
return self.model_dump_json(exclude=exclude, **kwargs)
return super().json(exclude=exclude, **kwargs)
if LATEST_PYDANTIC:
Vulnerability.model_rebuild()
else:
Vulnerability.update_forward_refs()
class ClosestSecureSpecification(SchemaModelV30):
upper: Optional[str]
lower: Optional[str]
class Remediation(BaseModel):
vulnerabilities_found: int
closest_secure: Annotated[
Optional[ClosestSecureSpecification], Field(alias="closest_secure_version")
] = None
recommended: Optional[str] = None
other_recommended: List[str] = []
class SpecificationVulnerabilities(BaseModel):
known_vulnerabilities: List[Vulnerability]
remediation: Optional[Remediation] = None
class AnalyzedSpecification(BaseModel):
raw: str
vulnerabilities: SpecificationVulnerabilities
class Package(BaseModel):
name: str
specifications: List[AnalyzedSpecification]
class FullPackage(Package):
found: str
class Results(BaseModel):
dependencies: List[Union[Package, FullPackage]] = []
class File(SchemaModelV30):
location: Annotated[
str,
Field(
...,
description="The full path of the file in the host machine.",
examples=["/Development/pyupio"],
title="Location",
),
]
type: Annotated[str, Field(..., examples=["pyproject.toml"], title="Type")]
categories: Annotated[
List[str], Field(..., examples=["python"], title="Categories")
]
results: Annotated[Results, Field(..., title="Results")]
class PolicySource(Enum):
local = "local"
cloud = "cloud"
class Meta(SchemaModelV30):
scan_type: Annotated[ScanType, Field()]
stage: Annotated[StageType, Field()] = StageType.development
scan_locations: Annotated[
List[str],
Field(
...,
description="Absolute path from where the scan was ran.",
examples=["/full-path"],
),
]
authenticated: Annotated[
bool,
Field(
...,
description="Indicate whether the scan report was ran by an "
"authenticated session. This is true if any "
"authentication method like token or api_key was used.",
examples=[True, False],
),
]
authentication_method: Annotated[Optional[AuthenticationMethod], Field()]
timestamp: Annotated[
datetime,
Field(..., examples=["2023-03-28T15:30:24"], title="Scan report generation."),
]
telemetry: Annotated[Telemetry, Field(..., title="Telemetry")]
schema_version: Annotated[Literal["3.0"], Field(...)] = "3.0"
class Policy(SchemaModelV30):
id: str
path: Optional[str] = Field(
...,
description="Relative path to the policy file used taking as a "
"reference the location property. It's null if the source is cloud.",
examples=[
".safety-policy.yml",
"../../upper_leve/.safety-general-policy.yml",
None,
],
title="Policy path",
)
source: PolicySource
class Projects(SchemaModelV30):
id: str = Field(
...,
description="This project id is tied and validated against "
"the Safety platform.",
title="Id",
)
upload_request_id: str
class ProjectsScan(SchemaModelV30):
id: str = Field(
...,
description="This project id is tied and validated "
"against the Safety platform.",
title="Id",
)
upload_request_id: Optional[str] = None
location: str = Field(
...,
description="Absolute path to the project root.",
examples=["/path-to-project-root"],
title="Location",
)
policy: Optional[Policy] = Field(
...,
description="Describe the policy used for this scan "
"in this particular project.",
title="Policy",
)
git: Optional[Git] = Field(..., title="Git")
files: List[File] = Field(..., title="Files")
class ScanResults(BaseModel):
files: Annotated[
List[File],
Field(
...,
description="Array of files found in an environment "
"scan with at least one finding of any type.",
title="Files",
),
] = []
projects: Annotated[
Union[List[Projects], List[ProjectsScan]],
Field(
...,
description="This list all the projects found with "
"its respective files or env results.",
title="Projects",
),
] = []
class Report(SchemaModelV30):
meta: Annotated[
Meta,
Field(
...,
description="Represents general data about the scan "
"executed in the target host.",
examples=["TODO, PENDING to add examples"],
title="Metadata",
),
]
scan_results: Annotated[ScanResults, Field(..., title="Scan Results")]
if LATEST_PYDANTIC:
@model_validator(mode="after")
def validate_projects(self):
return self._validate_projects(self)
else:
@classmethod
@model_validator(pre=True)
def validate_projects(cls, values):
return cls._validate_projects(cls(**values))
@staticmethod
def _validate_projects(report):
try:
scan_type = report.meta.scan_type
except Exception:
raise ValueError("Unable to parse meta from the file report.")
try:
projects = report.scan_results.projects
except Exception:
raise ValueError("Unable to parse projects from the file report.")
if scan_type is ScanType.scan and any(
isinstance(p, Projects) for p in projects
):
raise ValueError(
"If scan_type is 'scan', only ProjectsScan objects "
"should be used in results.projects."
)
if scan_type is ScanType.system_scan and any(
isinstance(p, ProjectsScan) for p in projects
):
raise ValueError(
"If scan_type is not 'scan', only Projects objects "
"should be used in results.projects."
)
return report