Skip to content
Merged
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
119 changes: 119 additions & 0 deletions dojo/announcement/os_message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import logging

import bleach
import markdown
import requests
from django.core.cache import cache

logger = logging.getLogger(__name__)

BUCKET_URL = "https://storage.googleapis.com/defectdojo-os-messages-prod/open_source_message.md"
CACHE_SECONDS = 3600
HTTP_TIMEOUT_SECONDS = 2
CACHE_KEY = "os_message:v1"

INLINE_TAGS = ["strong", "em", "a"]
INLINE_ATTRS = {"a": ["href", "title"]}

# Keep BLOCK_TAGS / BLOCK_ATTRS in sync with the DaaS publisher's
# MARKDOWNIFY["default"]["WHITELIST_TAGS"] / WHITELIST_ATTRS so previews
# on DaaS and rendering in OSS stay byte-identical.
BLOCK_TAGS = [
"p", "ul", "ol", "li", "a", "strong", "em", "code", "pre",
"blockquote", "h2", "h3", "h4", "hr", "br", "b", "i",
"abbr", "acronym",
]
BLOCK_ATTRS = {
"a": ["href", "title"],
"abbr": ["title"],
"acronym": ["title"],
}

_MISS = object()


def fetch_os_message():
cached = cache.get(CACHE_KEY, default=_MISS)
if cached is not _MISS:
return cached

try:
response = requests.get(BUCKET_URL, timeout=HTTP_TIMEOUT_SECONDS)
except Exception:
logger.debug("os_message: fetch failed", exc_info=True)
cache.set(CACHE_KEY, None, CACHE_SECONDS)
return None

if response.status_code != 200 or not response.text.strip():
cache.set(CACHE_KEY, None, CACHE_SECONDS)
return None

cache.set(CACHE_KEY, response.text, CACHE_SECONDS)
return response.text


def _strip_outer_p(html):
stripped = html.strip()
if stripped.startswith("<p>") and stripped.endswith("</p>"):
return stripped[3:-4]
return stripped


def parse_os_message(text):
lines = text.splitlines()

headline_source = None
body_start = None
for index, line in enumerate(lines):
if line.startswith("# "):
headline_source = line[2:].strip()
body_start = index + 1
break

if not headline_source:
return None

headline_source = headline_source[:100]
headline_rendered = markdown.markdown(headline_source)
headline_cleaned = bleach.clean(
headline_rendered,
tags=INLINE_TAGS,
attributes=INLINE_ATTRS,
strip=True,
)
headline_html = _strip_outer_p(headline_cleaned)

expanded_html = None
expanded_marker = "## Expanded Message"
expanded_body_lines = None
for offset, line in enumerate(lines[body_start:], start=body_start):
if line.strip() == expanded_marker:
expanded_body_lines = lines[offset + 1:]
break

if expanded_body_lines is not None:
expanded_source = "\n".join(expanded_body_lines).strip()
if expanded_source:
expanded_rendered = markdown.markdown(
expanded_source,
extensions=["extra", "fenced_code", "nl2br"],
)
expanded_html = bleach.clean(
expanded_rendered,
tags=BLOCK_TAGS,
attributes=BLOCK_ATTRS,
strip=True,
)

return {"message": headline_html, "expanded_html": expanded_html}


def get_os_banner():
try:
text = fetch_os_message()
if not text:
return None
return parse_os_message(text)
except Exception:
logger.debug("os_message: get_os_banner failed", exc_info=True)
return None
20 changes: 4 additions & 16 deletions dojo/announcement/signals.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver

Expand All @@ -7,22 +6,11 @@

@receiver(post_save, sender=Dojo_User)
def add_announcement_to_new_user(sender, instance, **kwargs):
announcements = Announcement.objects.all()
if announcements.count() > 0:
dojo_user = Dojo_User.objects.get(id=instance.id)
announcement = announcements.first()
cloud_announcement = (
"DefectDojo Pro Cloud and On-Premise Subscriptions Now Available!"
in announcement.message
announcement = Announcement.objects.first()
if announcement is not None:
UserAnnouncement.objects.get_or_create(
user=instance, announcement=announcement,
)
if not cloud_announcement or settings.CREATE_CLOUD_BANNER:
user_announcements = UserAnnouncement.objects.filter(
user=dojo_user, announcement=announcement,
)
if user_announcements.count() == 0:
UserAnnouncement.objects.get_or_create(
user=dojo_user, announcement=announcement,
)


@receiver(post_save, sender=Announcement)
Expand Down
26 changes: 24 additions & 2 deletions dojo/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
from django.conf import settings
from django.contrib import messages

from dojo.announcement.os_message import get_os_banner
from dojo.labels import get_labels
from dojo.models import Alerts, System_Settings, UserAnnouncement


def globalize_vars(request):
# return the value you want as a dictionnary. you may add multiple values in there.
return {
context = {
"SHOW_LOGIN_FORM": settings.SHOW_LOGIN_FORM,
"FORGOT_PASSWORD": settings.FORGOT_PASSWORD,
"FORGOT_USERNAME": settings.FORGOT_USERNAME,
Expand All @@ -35,11 +36,32 @@ def globalize_vars(request):
"DOCUMENTATION_URL": settings.DOCUMENTATION_URL,
"API_TOKENS_ENABLED": settings.API_TOKENS_ENABLED,
"API_TOKEN_AUTH_ENDPOINT_ENABLED": settings.API_TOKEN_AUTH_ENDPOINT_ENABLED,
"CREATE_CLOUD_BANNER": settings.CREATE_CLOUD_BANNER,
"SHOW_PLG_LINK": True,
# V3 Feature Flags
"V3_FEATURE_LOCATIONS": settings.V3_FEATURE_LOCATIONS,
}

additional_banners = []

if (os_banner := get_os_banner()) is not None:
additional_banners.append({
"source": "os",
"message": os_banner["message"],
"style": "info",
"url": "",
"link_text": "",
"expanded_html": os_banner["expanded_html"],
})

if hasattr(request, "session"):
for banner in request.session.pop("_product_banners", []):
additional_banners.append(banner)

if additional_banners:
context["additional_banners"] = additional_banners

return context


def bind_system_settings(request):
"""Load system settings and display warning if there's a database error."""
Expand Down
26 changes: 0 additions & 26 deletions dojo/management/commands/complete_initialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from django.db.utils import ProgrammingError

from dojo.auditlog import configure_pghistory_triggers
from dojo.models import Announcement, Dojo_User, UserAnnouncement


class Command(BaseCommand):
Expand All @@ -38,13 +37,11 @@ def handle(self, *args: Any, **options: Any) -> None:

if self.admin_user_exists():
self.stdout.write("Admin user already exists; skipping first-boot setup")
self.create_announcement_banner()
self.initialize_data()
return

self.ensure_admin_secrets()
self.first_boot_setup()
self.create_announcement_banner()
self.initialize_data()

# ------------------------------------------------------------------
Expand All @@ -58,29 +55,6 @@ def initialize_data(self) -> None:
self.stdout.write("Initializing non-standard permissions")
call_command("initialize_permissions")

def create_announcement_banner(self) -> None:
if os.getenv("DD_CREATE_CLOUD_BANNER"):
return

self.stdout.write("Creating announcement banner")

announcement, _ = Announcement.objects.get_or_create(id=1)
announcement.message = (
'<a href="https://cloud.defectdojo.com/accounts/onboarding/plg_step_1" '
'target="_blank">'
"DefectDojo Pro Cloud and On-Premise Subscriptions Now Available! "
"Create an account to try Pro for free!"
"</a>"
)
announcement.dismissable = True
announcement.save()

for user in Dojo_User.objects.all():
UserAnnouncement.objects.get_or_create(
user=user,
announcement=announcement,
)

# ------------------------------------------------------------------
# Auditlog consistency
# ------------------------------------------------------------------
Expand Down
31 changes: 14 additions & 17 deletions dojo/product_announcements.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@

import logging

from django.conf import settings
from django.contrib import messages
from django.http import HttpRequest, HttpResponse
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
Expand Down Expand Up @@ -30,12 +28,8 @@ def __init__(
response_data: dict | None = None,
**kwargs: dict,
):
"""Skip all this if the CREATE_CLOUD_BANNER is not set"""
if not settings.CREATE_CLOUD_BANNER:
return
# Fill in the vars if the were supplied correctly
if request is not None and isinstance(request, HttpRequest):
self._add_django_message(
self._add_session_banner(
request=request,
message=mark_safe(f"{self.base_message} {self.ui_outreach}"),
)
Expand All @@ -51,18 +45,21 @@ def __init__(
msg = "At least one of request, response, or response_data must be supplied"
raise ValueError(msg)

def _add_django_message(self, request: HttpRequest, message: str):
"""Add a message to the UI"""
def _add_session_banner(self, request: HttpRequest, message: str):
"""Store a banner in the session for rendering via additional_banners."""
try:
messages.add_message(
request=request,
level=messages.INFO,
message=_(message),
extra_tags="alert-info",
)
banners = request.session.get("_product_banners", [])
banners.append({
"source": "product_announcement",
"message": str(_(message)),
"style": "info",
"url": "",
"link_text": "",
"expanded_html": None,
})
request.session["_product_banners"] = banners
except Exception:
# make sure we catch any exceptions that might happen: https://github.com/DefectDojo/django-DefectDojo/issues/14041
logger.exception(f"Error adding message to Django: {message}")
logger.exception(f"Error storing product announcement banner: {message}")

def _add_api_response_key(self, message: str, data: dict) -> dict:
"""Update the response data in place"""
Expand Down
12 changes: 0 additions & 12 deletions dojo/settings/settings.dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,6 @@
DD_HASHCODE_FIELDS_PER_SCANNER=(str, ""),
# Set deduplication algorithms per parser, via en env variable that contains a JSON string
DD_DEDUPLICATION_ALGORITHM_PER_PARSER=(str, ""),
# Dictates whether cloud banner is created or not
DD_CREATE_CLOUD_BANNER=(bool, True),
# With this setting turned on, Dojo maintains an audit log of changes made to entities (Findings, Tests, Engagements, Products, ...)
# If you run big import you may want to disable this because there's a performance hit during (re-)imports.
DD_ENABLE_AUDITLOG=(bool, True),
Expand Down Expand Up @@ -1339,13 +1337,6 @@ def saml2_attrib_map_format(din):
"expires": int(60 * 1 * 1.2), # If a task is not executed within 72 seconds, it should be dropped from the queue. Two more tasks should be scheduled in the meantime.
},
},
"trigger_evaluate_pro_proposition": {
"task": "dojo.tasks.evaluate_pro_proposition",
"schedule": timedelta(hours=8),
"options": {
"expires": int(60 * 60 * 8 * 1.2), # If a task is not executed within 9.6 hours, it should be dropped from the queue. Two more tasks should be scheduled in the meantime.
},
},
"clear_sessions": {
"task": "dojo.tasks.clear_sessions",
"schedule": crontab(hour=0, minute=0, day_of_week=0),
Expand Down Expand Up @@ -2082,9 +2073,6 @@ def saml2_attrib_map_format(din):
AUDITLOG_DISABLE_ON_RAW_SAVE = False
# You can set extra Jira headers by suppling a dictionary in header: value format (pass as env var like "headr_name=value,another_header=anohter_value")
ADDITIONAL_HEADERS = env("DD_ADDITIONAL_HEADERS")
# Dictates whether cloud banner is created or not
CREATE_CLOUD_BANNER = env("DD_CREATE_CLOUD_BANNER")

# ------------------------------------------------------------------------------
# Auditlog
# ------------------------------------------------------------------------------
Expand Down
35 changes: 35 additions & 0 deletions dojo/static/dojo/css/dojo.css
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,41 @@ div.custom-search-form {
.announcement-banner {
margin: 0px -15px;
border-radius: 0px 0px 4px 4px;
color: #000;
}

.announcement-banner a {
color: #0645ad;
text-decoration: underline;
}

.announcement-banner strong,
.announcement-banner b {
color: #222;
}

.banner-toggle {
background: transparent;
border: 0;
padding: 0;
margin-left: 6px;
color: inherit;
cursor: pointer;
line-height: 1;
}

.banner-toggle:focus,
.banner-toggle:active {
outline: none;
box-shadow: none;
}

.banner-toggle:not(.collapsed) .fa-caret-down {
transform: rotate(180deg);
}

.banner-expanded {
margin-top: 8px;
}

@media (min-width: 795px) {
Expand Down
Loading
Loading