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
1 change: 1 addition & 0 deletions backend/apps/ai/common/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
DELIMITER = "\n\n"
GITHUB_REQUEST_INTERVAL_SECONDS = 0.5
MIN_REQUEST_INTERVAL_SECONDS = 1.2
QUEUE_RESPONSE_TIME_MINUTES = 1
7 changes: 4 additions & 3 deletions backend/apps/ai/common/extractors/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,10 @@ def extract_repository_content(repository) -> tuple[str, str]:
if response and is_valid_json(response):
items = json.loads(response)
for item in items:
name = item.get("name", "")
if name.startswith("tab_") and name.endswith(".md"):
tab_files.append(name)
if isinstance(item, dict):
name = item.get("name", "")
if name.startswith("tab_") and name.endswith(".md"):
tab_files.append(name)

all_markdown_files = markdown_files + tab_files

Expand Down
9 changes: 9 additions & 0 deletions backend/apps/slack/MANIFEST.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ features:
description: OWASP users list
usage_hint: <user>
should_escape: false
- command: /ai
url: https://nest.owasp.org/integrations/slack/commands/
description: AI-powered OWASP Nest assistant
usage_hint: <your question>
should_escape: false
oauth_config:
scopes:
user:
Expand All @@ -103,6 +108,7 @@ oauth_config:
- mpim:read
- users:read
bot:
- app_mentions:read
- channels:read
- chat:write
- commands
Expand All @@ -115,6 +121,7 @@ oauth_config:
- users:read
- groups:write
- channels:manage
- channels:history
settings:
event_subscriptions:
request_url: https://nest.owasp.org/integrations/slack/events/
Expand All @@ -123,7 +130,9 @@ settings:
- team_join
bot_events:
- app_home_opened
- app_mention
- member_joined_channel
- message.channels
- team_join
interactivity:
is_enabled: true
Expand Down
1 change: 1 addition & 0 deletions backend/apps/slack/admin/conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class ConversationAdmin(admin.ModelAdmin):
"is_private",
"is_archived",
"is_general",
"is_nest_bot_assistant_enabled",
)
},
),
Expand Down
7 changes: 7 additions & 0 deletions backend/apps/slack/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ class SlackConfig(AppConfig):
else None
)

def ready(self):
"""Configure Slack events when the app is ready."""
super().ready()
from apps.slack.events import configure_slack_events

configure_slack_events()


if SlackConfig.app:

Expand Down
1 change: 1 addition & 0 deletions backend/apps/slack/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from apps.slack.commands.command import CommandBase

from . import (
ai,
board,
chapters,
committees,
Expand Down
23 changes: 23 additions & 0 deletions backend/apps/slack/commands/ai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Slack bot AI command."""

from apps.slack.commands.command import CommandBase


class Ai(CommandBase):
"""Slack bot /ai command."""

def render_blocks(self, command: dict):
"""Get the rendered blocks.

Args:
command (dict): The Slack command payload.

Returns:
list: A list of Slack blocks representing the AI response.

"""
from apps.slack.common.handlers.ai import get_blocks

return get_blocks(
query=command["text"].strip(),
)
61 changes: 61 additions & 0 deletions backend/apps/slack/common/handlers/ai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Handler for AI-powered Slack functionality."""

from __future__ import annotations

import logging

from apps.ai.agent.tools.rag.rag_tool import RagTool
from apps.slack.blocks import markdown

logger = logging.getLogger(__name__)


def get_blocks(query: str) -> list[dict]:
"""Get AI response blocks.

Args:
query (str): The user's question.
presentation (EntityPresentation | None): Configuration for entity presentation.

Returns:
list: A list of Slack blocks representing the AI response.

"""
ai_response = process_ai_query(query.strip())

if ai_response:
return [markdown(ai_response)]
return get_error_blocks()


def process_ai_query(query: str) -> str | None:
"""Process the AI query using the RAG tool.

Args:
query (str): The user's question.

Returns:
str | None: The AI response or None if error occurred.

"""
rag_tool = RagTool(
chat_model="gpt-4o",
embedding_model="text-embedding-3-small",
)

return rag_tool.query(question=query)


def get_error_blocks() -> list[dict]:
"""Get error response blocks.

Returns:
list: A list of Slack blocks with error message.

"""
return [
markdown(
"⚠️ Unfortunately, I'm unable to answer your question at this time.\n"
"Please try again later or contact support if the issue persists."
)
]
141 changes: 141 additions & 0 deletions backend/apps/slack/common/question_detector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""Question detection utilities for Slack OWASP bot."""

from __future__ import annotations

import logging
import os
import re

import openai

from apps.slack.constants import OWASP_KEYWORDS

logger = logging.getLogger(__name__)


class QuestionDetector:
"""Utility class for detecting OWASP-related questions."""

MAX_TOKENS = 50
TEMPERATURE = 0.1
CHAT_MODEL = "gpt-4o"

SYSTEM_PROMPT = """
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have Prompt models for this.

You are an expert in cybersecurity and OWASP (Open Web Application Security Project).
Your task is to determine if a given question is related to OWASP, cybersecurity,
web application security, or similar topics.
Key OWASP-related terms: {keywords}
Respond with only "YES" if the question is related to OWASP/cybersecurity,
or "NO" if it's not.
Do not provide any explanation or additional text.
"""

def __init__(self):
"""Initialize the question detector.
Raises:
ValueError: If the OpenAI API key is not set.
"""
if not (openai_api_key := os.getenv("DJANGO_OPEN_AI_SECRET_KEY")):
error_msg = "DJANGO_OPEN_AI_SECRET_KEY environment variable not set"
raise ValueError(error_msg)

self.owasp_keywords = OWASP_KEYWORDS
self.openai_client = openai.OpenAI(api_key=openai_api_key)

question_patterns = [
r"\?",
r"^(what|how|why|when|where|which|who|can|could|would|should|is|are|does|do|did)",
r"(help|explain|tell me|show me|guide|tutorial|example)",
r"(recommend|suggest|advice|opinion)",
]

self.compiled_patterns = [
re.compile(pattern, re.IGNORECASE) for pattern in question_patterns
]

def is_owasp_question(self, text: str) -> bool:
"""Check if the input text is an OWASP-related question.
This is the main public method that orchestrates the detection logic.
"""
if not text or not text.strip():
return False

if not self.is_question(text):
return False

openai_result = self.is_owasp_question_with_openai(text)

if openai_result is None:
logger.warning(
"OpenAI detection failed. Falling back to keyword matching",
)
return self.contains_owasp_keywords(text)

if openai_result:
return True
if self.contains_owasp_keywords(text):
logger.info(
"OpenAI classified as non-OWASP, but keywords were detected. Overriding to TRUE."
)
return True
return False

def is_question(self, text: str) -> bool:
"""Check if text appears to be a question."""
return any(pattern.search(text) for pattern in self.compiled_patterns)

def is_owasp_question_with_openai(self, text: str) -> bool | None:
"""Determine if the text is an OWASP-related question.
Returns:
- True: If the model responds with "YES".
- False: If the model responds with "NO".
- None: If the API call fails or the response is unexpected.
"""
system_prompt = self.SYSTEM_PROMPT.format(keywords=", ".join(self.owasp_keywords))
Copy link
Collaborator

@arkid15r arkid15r Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What content / knowledge base is used to determine the answer?

user_prompt = f'Question: "{text}"'

try:
response = self.openai_client.chat.completions.create(
model=self.CHAT_MODEL,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
temperature=self.TEMPERATURE,
max_tokens=self.MAX_TOKENS,
)
except openai.OpenAIError:
logger.exception("OpenAI API error during question detection")
return None
else:
answer = response.choices[0].message.content
if not answer:
logger.error("OpenAI returned an empty response")
return None

clean_answer = answer.strip().upper()

if "YES" in clean_answer:
return True
if "NO" in clean_answer:
return False
logger.warning("Unexpected OpenAI response")
return None

def contains_owasp_keywords(self, text: str) -> bool:
"""Check if text contains OWASP-related keywords."""
words = re.findall(r"\b\w+\b", text)
text_words = set(words)

intersection = self.owasp_keywords.intersection(text_words)
if intersection:
return True

return any(" " in keyword and keyword in text for keyword in self.owasp_keywords)
58 changes: 58 additions & 0 deletions backend/apps/slack/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,64 @@
OWASP_SPONSORSHIP_CHANNEL_ID = "#C08EGFDD9L2"
OWASP_THREAT_MODELING_CHANNEL_ID = "#C1CS3C6AF"

OWASP_KEYWORDS = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach doesn't scale well with OWASP content growing.

"api security",
"appsec",
"application security",
"assessment",
"authentication",
"authorization",
"cheat sheet series",
"chapter",
"code review",
"committee",
"cryptography",
"csrf",
"defectdojo",
"dependency",
"devops",
"devsecops",
"dynamic analysis",
"encryption",
"event",
"firewall",
"injection",
"juice shop",
"mobile security",
"nest",
"nettacker",
"owasp",
"penetration",
"project",
"rasp",
"red team",
"risk",
"sbom",
"secure",
"secure coding",
"security",
"security best practice",
"security bug",
"security fix",
"security framework",
"security guideline",
"security patch",
"security policy",
"security standard",
"security testing",
"security tools",
"static analysis",
"threat",
"threat modeling",
"top 10",
"top10",
"vulnerabilities",
"vulnerability",
"web security",
"webgoat",
"xss",
}

OWASP_WORKSPACE_ID = "T04T40NHX"

VIEW_PROJECTS_ACTION = "view_projects_action"
Expand Down
Loading