Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions scripts/snippets/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,15 @@ class SnippetSourceAttributeValidation:
| None
)
issues: tuple[SnippetSourceAttributeIssue, ...]


class SnippetSourceSafetyRule(str, Enum):
UNREGISTERED_REPOSITORY = "unregistered_repository"
UNSAFE_PATH = "unsafe_path"


@dataclass(frozen=True)
class SnippetSourceSafetyIssue:
rule: SnippetSourceSafetyRule
span: Span
message: str
36 changes: 36 additions & 0 deletions scripts/snippets/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
IfVersionAttributeValidation,
IfVersionCondition,
IfVersionTag,
ImmutableSourceReference,
LocalSourceReference,
PullRequestSnippetSource,
SnippetSourceSafetyIssue,
SnippetSourceSafetyRule,
SnippetTag,
Span,
)
from .references import parse_github_repository_url
from .registry import RepositoryRegistry
Expand Down Expand Up @@ -255,3 +261,33 @@ def unknown_snippet_attributes(tag: SnippetTag) -> tuple[str, ...]:
if attribute.name not in SNIPPET_ATTRIBUTES
)
)


def validate_snippet_source_safety(
source: (
ImmutableSourceReference | PullRequestSnippetSource | LocalSourceReference
),
*,
span: Span,
registry: RepositoryRegistry,
) -> tuple[SnippetSourceSafetyIssue, ...]:
"""Apply repository allowlist and path-safety policy to a resolved source."""

issues: list[SnippetSourceSafetyIssue] = []
if not is_registered_repository(source.repository, registry):
issues.append(
SnippetSourceSafetyIssue(
rule=SnippetSourceSafetyRule.UNREGISTERED_REPOSITORY,
span=span,
message=f"Repository {source.repository!r} is not allowlisted",
)
)
if not is_safe_source_path(source.path):
issues.append(
SnippetSourceSafetyIssue(
rule=SnippetSourceSafetyRule.UNSAFE_PATH,
span=span,
message=f"Unsafe source path {source.path!r}",
)
)
return tuple(issues)
66 changes: 66 additions & 0 deletions tests/test_snippet_source_safety_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from __future__ import annotations

import pytest

from scripts.snippets.model import (
ImmutableSourceReference,
SnippetSourceSafetyIssue,
SnippetSourceSafetyRule,
Span,
)
from scripts.snippets.registry import (
RepositoryConfig,
RepositoryRegistry,
RepositoryVisibility,
)
from scripts.snippets.validation import validate_snippet_source_safety

REGISTRY = RepositoryRegistry(
(
RepositoryConfig(
name="canton-network/splice",
url="https://github.com/canton-network/splice",
default_branch="main",
visibility=RepositoryVisibility.PUBLIC,
),
)
)
SPAN = Span(start=0, end=1, line=1, column=1)
COMMIT = "2c941ea9e834d7602d388f3271c0f864025ea756"


def validate(
repository: str, path: str
) -> tuple[SnippetSourceSafetyIssue, ...]:
return validate_snippet_source_safety(
ImmutableSourceReference(
repository=repository, commit=COMMIT, path=path
),
span=SPAN,
registry=REGISTRY,
)


def test_accepts_allowlisted_repository_and_safe_path() -> None:
assert validate("canton-network/splice", "apps/file.yaml") == ()


@pytest.mark.parametrize(
("repository", "path", "rule"),
[
(
"unknown/repository",
"apps/file.yaml",
SnippetSourceSafetyRule.UNREGISTERED_REPOSITORY,
),
(
"canton-network/splice",
"../secret",
SnippetSourceSafetyRule.UNSAFE_PATH,
),
],
)
def test_rejects_unregistered_repository_or_unsafe_path(
repository: str, path: str, rule: SnippetSourceSafetyRule
) -> None:
assert rule in {issue.rule for issue in validate(repository, path)}