Skip to content
Open
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
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Nixpkgs security tracker contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
1 change: 0 additions & 1 deletion infra/production.nix
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ in
GH_SECURITY_TEAM = "security";
GH_COMMITTERS_TEAM = "nixpkgs-committers";
GH_ISSUES_LABELS = [ "1.severity: security" ];
GH_ISSUES_COMMITTERS_ONLY = true;
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend";
EMAIL_HOST = "umbriel.nixos.org";
EMAIL_PORT = 465;
Expand Down
9 changes: 0 additions & 9 deletions src/project/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,6 @@ class DjangoSettings(BaseModel):
This is used as a safety measure during development. Set to True in production.
"""
)
GH_ISSUES_COMMITTERS_ONLY: bool = Field(
description="""
When set to True, only committers and admins can publish
suggestions to create GitHub issues. This also affects who can
create suggestions and add/remove maintainers.
Setting this to False, will also allow maintainers to create issues.
""",
default=True,
)
GH_ORGANIZATION: str = Field(
description="""
The GitHub organisation from which to get team membership.
Expand Down
8 changes: 2 additions & 6 deletions src/shared/auth/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,5 @@ def ismaintainer(user: Any) -> bool:
).exists()


def can_publish_github_issue(user: Any) -> bool:
return (
isadmin(user)
or iscommitter(user)
or (not settings.GH_ISSUES_COMMITTERS_ONLY and ismaintainer(user))
)
def can_edit_suggestion(user: Any) -> bool:
return isadmin(user) or iscommitter(user)
24 changes: 12 additions & 12 deletions src/shared/fetchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ def make_media(data: dict[str, str]) -> models.SupportingMedia:


def make_description(data: dict[str, Any]) -> models.Description:
obj, created = models.Description.objects.get_or_create(
lang=data["lang"],
value=data["value"],
)
if created:
obj.media.set(map(make_media, data.get("supportingMedia", [])))
ctx: dict[str, Any] = dict()
ctx["lang"] = data["lang"]
ctx["value"] = data["value"]

obj = models.Description.objects.create(**ctx)
obj.media.set(map(make_media, data.get("supportingMedia", [])))

return obj

Expand All @@ -61,12 +61,12 @@ def make_tag(name: str) -> models.Tag:


def make_reference(data: dict[str, Any]) -> models.Reference:
obj, created = models.Reference.objects.get_or_create(
url=data["url"],
name=data.get("name", ""),
)
if created:
obj.tags.set(map(make_tag, data.get("tags", [])))
ctx: dict[str, Any] = dict()
ctx["url"] = data["url"]
ctx["name"] = data.get("name", "")

obj = models.Reference.objects.create(**ctx)
obj.tags.set(map(make_tag, data.get("tags", [])))

return obj

Expand Down
12 changes: 12 additions & 0 deletions src/shared/listeners/automatic_linkage.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ def build_new_links(container: Container) -> bool:
logger.info("Suggestion already exists for '%s', skipping", container.cve)
return False

if container.tags.filter(value="exclusively-hosted-service").exists():
logger.info(
"Container for '%s' is exclusively-hosted-service, rejecting without match.",
container.cve,
)
CVEDerivationClusterProposal.objects.create(
cve=container.cve,
status=CVEDerivationClusterProposal.Status.REJECTED,
rejection_reason=CVEDerivationClusterProposal.RejectionReason.EXCLUSIVELY_HOSTED_SERVICE,
)
return True

drvs = produce_linkage_candidates(container)
if not drvs:
logger.info("No derivations matching '%s', ignoring", container.cve)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("shared", "0069_remove_nixderivation_dependencies_and_more"),
]

operations = [
migrations.AddField(
model_name="cvederivationclusterproposal",
name="rejection_reason",
field=models.CharField(
blank=True,
choices=[
("exclusively_hosted_service", "exclusively hosted service"),
],
help_text="Machine-generated reason for automatic rejection",
max_length=126,
null=True,
),
),
]
14 changes: 14 additions & 0 deletions src/shared/models/linkage.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ class Status(models.TextChoices):
ACCEPTED = "accepted", _("accepted")
PUBLISHED = "published", _("published")

class RejectionReason(models.TextChoices):
EXCLUSIVELY_HOSTED_SERVICE = (
"exclusively_hosted_service",
_("exclusively hosted service"),
)

cached: "shared.models.cached.CachedSuggestions"

cve = models.ForeignKey(
Expand All @@ -52,6 +58,14 @@ class Status(models.TextChoices):
),
)

rejection_reason = models.CharField(
max_length=126,
choices=RejectionReason.choices,
null=True,
blank=True,
help_text=_("Machine-generated reason for automatic rejection"),
)

@property
def is_editable(self) -> bool:
return self.status in [
Expand Down
2 changes: 2 additions & 0 deletions src/shared/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ <h1>
</div>
{% endif %}

{% block workflow %}
<nav id="menu-bar">
<ul class="row gap">
<li class="row gap-small centered"><i class="icon-bin"></i><a href="{% url 'webview:suggestion:dismissed_suggestions' %}">Dismissed suggestions</a></li>
Expand All @@ -95,6 +96,7 @@ <h1>
<li class="row gap-small centered"><i class="icon-github"></i><a href="{% url 'webview:issue_list' %}">Published issues</a></li>
</ul>
</nav>
{% endblock %}

{% block layout %}
<main id="page-content">
Expand Down
26 changes: 0 additions & 26 deletions src/shared/tests/test_fetchers.py

This file was deleted.

22 changes: 21 additions & 1 deletion src/shared/tests/test_linkage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from shared.listeners.automatic_linkage import build_new_links
from shared.listeners.cache_suggestions import cache_new_suggestions
from shared.models.cve import Container
from shared.models.cve import Container, Tag
from shared.models.linkage import (
CVEDerivationClusterProposal,
DerivationClusterProposalLink,
Expand Down Expand Up @@ -123,3 +123,23 @@ def test_link_product_or_package_name(
cache_new_suggestions(link.proposal)
else:
assert not match


def test_exclusively_hosted_service_creates_rejected_proposal(
make_container: Callable[..., Container],
) -> None:
"""Containers tagged exclusively-hosted-service must be stored but immediately rejected."""
container = make_container()
tag, _ = Tag.objects.get_or_create(value="exclusively-hosted-service")
container.tags.add(tag)

result = build_new_links(container)

assert result is True
proposal = CVEDerivationClusterProposal.objects.get(cve=container.cve)
assert proposal.status == CVEDerivationClusterProposal.Status.REJECTED
assert (
proposal.rejection_reason
== CVEDerivationClusterProposal.RejectionReason.EXCLUSIVELY_HOSTED_SERVICE
)
assert proposal.derivations.count() == 0
6 changes: 5 additions & 1 deletion src/webview/notifications/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ class NotificationCenterView(LoginRequiredMixin, ListView):
def get_queryset(self) -> QuerySet[Notification]:
return (
Notification.objects.filter(user=self.request.user)
.select_related("user__profile", "suggestionnotification__suggestion__cve")
.select_related(
"user__profile",
"suggestionnotification__suggestion__cve",
"suggestionnotification__suggestion__cached",
)
.order_by("-created_at")
.select_subclasses()
)
Expand Down
88 changes: 0 additions & 88 deletions src/webview/suggestions/context/builders.py

This file was deleted.

Loading