Skip to content

Commit 6907f21

Browse files
Merge pull request #225 from RedHatProductSecurity/capture-feedback
feedback: add api/v1/feedback
2 parents e849764 + 3761d3e commit 6907f21

File tree

10 files changed

+318
-10
lines changed

10 files changed

+318
-10
lines changed

.env-example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
AEGIS_CLI_FEATURE_AGENT="public"
88
AEGIS_WEB_FEATURE_AGENT="public"
99

10+
# AEGIS_WEB_FEEDBACK_LOG=/tmp/feedback.log
11+
1012
# enable context tools
1113
AEGIS_USE_TAVILY_TOOL_CONTEXT=false
1214
# TAVILY_API_KEY=

docs/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [0.3.1]
88

9+
### Added
10+
- add `api/v1/feedback` REST api endpoint (and environment variable AEGIS_WEB_FEEDBACK_LOG)
11+
912
### Fixed
1013
- use stable version string in stable container images
1114

docs/env-vars.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717

1818

1919
# REST API settings
20-
| Environment Variable | Description | Default Value |
21-
|----------------------------|-------------------------------------------|-------------------------|
22-
| `AEGIS_WEB_FEATURE_AGENT` | Set to `redhat` to use rh profile | `public` |
23-
| `AEGIS_WEB_SPN` | Service Principal Name for Kerberos auth | |
24-
| `KRB5_KTNAME` | Path to the keytab file for Kerberos auth | `/etc/krb5.keytab` |
25-
| `AEGIS_CORS_TARGET_URL` | CORS origin url | `http://localhost:5173` |
20+
| Environment Variable | Description | Default Value |
21+
|----------------------------|-------------------------------------------|-----------------------------------|
22+
| `AEGIS_WEB_FEATURE_AGENT` | Set to `redhat` to use rh profile | `public` |
23+
| `AEGIS_WEB_SPN` | Service Principal Name for Kerberos auth | |
24+
| `KRB5_KTNAME` | Path to the keytab file for Kerberos auth | `/etc/krb5.keytab` |
25+
| `AEGIS_CORS_TARGET_URL` | CORS origin url | `http://localhost:5173` |
26+
| `AEGIS_WEB_FEEDBACK_LOG` | Feedback log file | `~/.config/aegis_ai/feedback.log` |
2627

2728

2829
# Tool settings

docs/openapi.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"openapi":"3.1.0","info":{"title":"Aegis REST-API","description":"A simple web console and REST API for Aegis.","version":"v1"},"paths":{"/":{"get":{"summary":"Home","operationId":"home__get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/console":{"get":{"summary":"Console","operationId":"console_console_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"post":{"summary":"Generate Response","description":"Handles the submission of a prompt, simulates an LLM response,\nand re-renders the console with the results.","operationId":"generate_response_console_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_generate_response_console_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analysis/cve":{"get":{"summary":"Cve Analysis","operationId":"cve_analysis_api_v1_analysis_cve_get","parameters":[{"name":"feature","in":"query","required":true,"schema":{"$ref":"#/components/schemas/aegis_ai_web__src__main__ComponentFeatureName__1"}},{"name":"cve_id","in":"query","required":true,"schema":{"type":"string","pattern":"^CVE-[0-9]{4}-[0-9]{4,7}$","title":"Cve Id"}},{"name":"detail","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Detail"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analysis/cve/{feature}":{"post":{"summary":"Cve Analysis With Body","operationId":"cve_analysis_with_body_api_v1_analysis_cve__feature__post","parameters":[{"name":"feature","in":"path","required":true,"schema":{"$ref":"#/components/schemas/aegis_ai_web__src__main__ComponentFeatureName__1"}},{"name":"detail","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Detail"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analysis/component":{"get":{"summary":"Component Analysis","operationId":"component_analysis_api_v1_analysis_component_get","parameters":[{"name":"feature","in":"query","required":true,"schema":{"$ref":"#/components/schemas/aegis_ai_web__src__main__ComponentFeatureName__2"}},{"name":"component_name","in":"query","required":true,"schema":{"type":"string","title":"Component Name"}},{"name":"detail","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Detail"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"Body_generate_response_console_post":{"properties":{"user_instruction":{"type":"string","title":"User Instruction"},"goals":{"type":"string","title":"Goals"},"rules":{"type":"string","title":"Rules"}},"type":"object","required":["user_instruction","goals","rules"],"title":"Body_generate_response_console_post"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"aegis_ai_web__src__main__ComponentFeatureName__1":{"type":"string","enum":["suggest-impact","suggest-cwe","rewrite-description","rewrite-statement","identify-pii","cvss-diff-explainer"],"title":"ComponentFeatureName"},"aegis_ai_web__src__main__ComponentFeatureName__2":{"type":"string","enum":["component-intelligence"],"title":"ComponentFeatureName"}}}}
1+
{"openapi":"3.1.0","info":{"title":"Aegis REST-API","description":"A simple web console and REST API for Aegis.","version":"v1"},"paths":{"/":{"get":{"summary":"Home","operationId":"home__get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/console":{"get":{"summary":"Console","operationId":"console_console_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"post":{"summary":"Generate Response","description":"Handles the submission of a prompt, simulates an LLM response,\nand re-renders the console with the results.","operationId":"generate_response_console_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_generate_response_console_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analysis/cve":{"get":{"summary":"Cve Analysis","operationId":"cve_analysis_api_v1_analysis_cve_get","parameters":[{"name":"feature","in":"query","required":true,"schema":{"$ref":"#/components/schemas/aegis_ai_web__src__main__ComponentFeatureName__1"}},{"name":"cve_id","in":"query","required":true,"schema":{"type":"string","pattern":"^CVE-[0-9]{4}-[0-9]{4,7}$","title":"Cve Id"}},{"name":"detail","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Detail"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analysis/cve/{feature}":{"post":{"summary":"Cve Analysis With Body","operationId":"cve_analysis_with_body_api_v1_analysis_cve__feature__post","parameters":[{"name":"feature","in":"path","required":true,"schema":{"$ref":"#/components/schemas/aegis_ai_web__src__main__ComponentFeatureName__1"}},{"name":"detail","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Detail"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/analysis/component":{"get":{"summary":"Component Analysis","operationId":"component_analysis_api_v1_analysis_component_get","parameters":[{"name":"feature","in":"query","required":true,"schema":{"$ref":"#/components/schemas/aegis_ai_web__src__main__ComponentFeatureName__2"}},{"name":"component_name","in":"query","required":true,"schema":{"type":"string","title":"Component Name"}},{"name":"detail","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Detail"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/feedback":{"post":{"summary":"Save Feedback","description":"Receive feedback, validate, sanitize, and log it to a separate log file.","operationId":"save_feedback_api_v1_feedback_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Feedback"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"Body_generate_response_console_post":{"properties":{"user_instruction":{"type":"string","title":"User Instruction"},"goals":{"type":"string","title":"Goals"},"rules":{"type":"string","title":"Rules"}},"type":"object","required":["user_instruction","goals","rules"],"title":"Body_generate_response_console_post"},"Feedback":{"properties":{"feature":{"type":"string","maxLength":100,"title":"Feature"},"cve_id":{"anyOf":[{"type":"string","maxLength":50,"pattern":"^CVE-[0-9]{4}-[0-9]{4,7}$"},{"type":"null"}],"title":"Cve Id","default":""},"email":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Email","default":""},"request_time":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Request Time","default":""},"actual":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Actual","default":""},"expected":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Expected","default":""},"accept":{"type":"boolean","title":"Accept","default":false}},"type":"object","required":["feature"],"title":"Feedback","description":"Data structure for feedback."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"aegis_ai_web__src__main__ComponentFeatureName__1":{"type":"string","enum":["suggest-impact","suggest-cwe","rewrite-description","rewrite-statement","identify-pii","cvss-diff-explainer"],"title":"ComponentFeatureName"},"aegis_ai_web__src__main__ComponentFeatureName__2":{"type":"string","enum":["component-intelligence"],"title":"ComponentFeatureName"}}}}

docs/openapi.yml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,57 @@ components:
1717
- rules
1818
title: Body_generate_response_console_post
1919
type: object
20+
Feedback:
21+
description: Data structure for feedback.
22+
properties:
23+
accept:
24+
default: false
25+
title: Accept
26+
type: boolean
27+
actual:
28+
anyOf:
29+
- maxLength: 50
30+
type: string
31+
- type: 'null'
32+
default: ''
33+
title: Actual
34+
cve_id:
35+
anyOf:
36+
- maxLength: 50
37+
pattern: ^CVE-[0-9]{4}-[0-9]{4,7}$
38+
type: string
39+
- type: 'null'
40+
default: ''
41+
title: Cve Id
42+
email:
43+
anyOf:
44+
- maxLength: 100
45+
type: string
46+
- type: 'null'
47+
default: ''
48+
title: Email
49+
expected:
50+
anyOf:
51+
- maxLength: 50
52+
type: string
53+
- type: 'null'
54+
default: ''
55+
title: Expected
56+
feature:
57+
maxLength: 100
58+
title: Feature
59+
type: string
60+
request_time:
61+
anyOf:
62+
- maxLength: 50
63+
type: string
64+
- type: 'null'
65+
default: ''
66+
title: Request Time
67+
required:
68+
- feature
69+
title: Feedback
70+
type: object
2071
HTTPValidationError:
2172
properties:
2273
detail:
@@ -179,6 +230,30 @@ paths:
179230
$ref: '#/components/schemas/HTTPValidationError'
180231
description: Validation Error
181232
summary: Cve Analysis With Body
233+
/api/v1/feedback:
234+
post:
235+
description: Receive feedback, validate, sanitize, and log it to a separate
236+
log file.
237+
operationId: save_feedback_api_v1_feedback_post
238+
requestBody:
239+
content:
240+
application/json:
241+
schema:
242+
$ref: '#/components/schemas/Feedback'
243+
required: true
244+
responses:
245+
'200':
246+
content:
247+
application/json:
248+
schema: {}
249+
description: Successful Response
250+
'422':
251+
content:
252+
application/json:
253+
schema:
254+
$ref: '#/components/schemas/HTTPValidationError'
255+
description: Validation Error
256+
summary: Save Feedback
182257
/console:
183258
get:
184259
operationId: console_console_get

src/aegis_ai_web/src/__init__.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,95 @@
1-
# REST API version
1+
# REST API
2+
import logging
23
import os
4+
import re
5+
from logging.handlers import RotatingFileHandler
6+
from typing import Optional
7+
8+
from pydantic import BaseModel, Field, field_validator
9+
10+
from aegis_ai import config_dir
11+
from aegis_ai.data_models import CVEID
312

413
AEGIS_REST_API_VERSION: str = "v1"
514

615
feature_agent = os.getenv("AEGIS_WEB_FEATURE_AGENT", "public")
16+
feedback_log = os.getenv("AEGIS_WEB_FEEDBACK_LOG", f"{config_dir}/feedback.log")
17+
18+
19+
class Feedback(BaseModel):
20+
"""
21+
Data structure for feedback.
22+
"""
23+
24+
feature: str = Field(..., max_length=100)
25+
26+
@field_validator("feature")
27+
@classmethod
28+
def sanitize_feature(cls, feature: str) -> str:
29+
return sanitize_input(feature)
30+
31+
cve_id: Optional[CVEID] = Field("", max_length=50)
32+
33+
email: Optional[str] = Field("", max_length=100)
34+
35+
@field_validator("email")
36+
@classmethod
37+
def sanitize_email(cls, email: str) -> str:
38+
return sanitize_input(email)
39+
40+
request_time: Optional[str] = Field("", max_length=50)
41+
42+
@field_validator("request_time")
43+
@classmethod
44+
def sanitize_request_time(cls, request_time: str) -> str:
45+
return sanitize_input(request_time)
46+
47+
actual: Optional[str] = Field("", max_length=50)
48+
49+
@field_validator("actual")
50+
@classmethod
51+
def sanitize_actual(cls, actual: str) -> str:
52+
return sanitize_input(actual)
53+
54+
expected: Optional[str] = Field("", max_length=50)
55+
56+
@field_validator("expected")
57+
@classmethod
58+
def sanitize_expected(cls, expected: str) -> str:
59+
return sanitize_input(expected)
60+
61+
accept: bool = Field(False)
62+
63+
64+
def sanitize_input(text) -> str:
65+
"""
66+
basic content sanitize.
67+
"""
68+
69+
if text:
70+
# The regex "[^a-zA-Z0-9, -,^\x20-\x7E]" does:
71+
# a-zA-Z0-9: allow all uppercase letters, lowercase letters, and digits.
72+
# -: allow hyphen.
73+
# @: allow apersand
74+
return re.sub(r"[^a-zA-Z0-9,-, @]", "", text)
75+
return ""
76+
77+
78+
def setup_feedback_logger(level=logging.INFO):
79+
"""Setup feedback logger."""
80+
81+
handler = RotatingFileHandler(
82+
feedback_log,
83+
maxBytes=10 * 1024 * 1024,
84+
backupCount=5,
85+
)
86+
formatter = logging.Formatter("%(asctime)s - %(message)s")
87+
88+
handler.setFormatter(formatter)
89+
logger = logging.getLogger("feedback_logger")
90+
logger.setLevel(level)
91+
92+
if not logger.handlers:
93+
logger.addHandler(handler)
94+
95+
return logger

src/aegis_ai_web/src/main.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import yaml
1414
from fastapi import FastAPI, Request, HTTPException, Form
1515
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, Response
16+
1617
from starlette.middleware.base import BaseHTTPMiddleware
1718
from fastapi.middleware.cors import CORSMiddleware
1819

@@ -26,7 +27,12 @@
2627
from aegis_ai.features import cve, component
2728
from aegis_ai.features.data_models import AegisAnswer
2829

29-
from . import AEGIS_REST_API_VERSION, feature_agent
30+
from . import (
31+
AEGIS_REST_API_VERSION,
32+
feature_agent,
33+
setup_feedback_logger,
34+
Feedback,
35+
)
3036

3137

3238
class HSTSHeaderMiddleware(BaseHTTPMiddleware):
@@ -42,6 +48,8 @@ async def dispatch(self, request: Request, call_next):
4248

4349
config_logging()
4450

51+
feedback_logger = setup_feedback_logger()
52+
4553
app = FastAPI(
4654
title="Aegis REST-API",
4755
description="A simple web console and REST API for Aegis.",
@@ -267,3 +275,30 @@ async def component_analysis(
267275
raise HTTPException(
268276
500, detail=f"Error executing Component feature '{feature}': {e}"
269277
)
278+
279+
280+
@app.post("/api/v1/feedback")
281+
async def save_feedback(feedback: Feedback):
282+
"""
283+
Receive feedback, validate, sanitize, and log it to a separate log file.
284+
"""
285+
286+
try:
287+
log_message = (
288+
f"FEEDBACK RECEIVED | "
289+
f"feature: '{feedback.feature}' | "
290+
f"cve_id: '{feedback.cve_id}' | "
291+
f"actual: '{feedback.actual}' | "
292+
f"expected: '{feedback.expected}' | "
293+
f"request_time: '{feedback.request_time}' | "
294+
f"accept: '{feedback.accept}'"
295+
)
296+
feedback_logger.info(log_message)
297+
return {"status": "Feedback received and logged successfully."}
298+
299+
except Exception as e:
300+
logging.error(f"Failed to process feedback: {e}", exc_info=True)
301+
raise HTTPException(
302+
status_code=500,
303+
detail="An internal error occurred while processing feedback.",
304+
)

src/aegis_ai_web/tests/conftest.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import pytest
2+
from pathlib import Path
3+
4+
5+
@pytest.fixture(autouse=True)
6+
def feedback_log_setup(tmp_path: Path, monkeypatch):
7+
"""
8+
Create unique temp log file for each test function,
9+
set AEGIS_WEB_FEEDBACK_LOG env var.
10+
11+
Note: pytest does automatic cleanup.
12+
"""
13+
log_file_path = tmp_path / "test_feedback.log"
14+
monkeypatch.setenv("AEGIS_WEB_FEEDBACK_LOG", str(log_file_path))
15+
16+
yield log_file_path

0 commit comments

Comments
 (0)