Skip to content

Commit e12fec7

Browse files
committed
backup commit
1 parent 3113c63 commit e12fec7

97 files changed

Lines changed: 5033 additions & 1746 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import json
2+
import logging
3+
import os
4+
from tempfile import NamedTemporaryFile
5+
from typing import Dict, List, Optional
6+
from uuid import uuid4
7+
8+
from analyticq.manager.tool_manager import AnalyticQSASTManager
9+
from analyticq.schemas.analyse_dto import AnalysisResponse, GitRepoRequest
10+
from fastapi import (APIRouter, BackgroundTasks, File, Form, HTTPException,
11+
UploadFile)
12+
13+
logger = logging.getLogger(__name__)
14+
analyze_router = APIRouter(prefix="/analyze", tags=["Analysis"])
15+
sast_manager = AnalyticQSASTManager()
16+
17+
# In-memory analysis tracking (consider replacing with persistent storage in production)
18+
analysis_results = {}
19+
20+
21+
async def _run_git_analysis(
22+
analysis_id: str,
23+
git_url: str,
24+
branch: Optional[str] = None,
25+
config_paths: Optional[Dict[str, str]] = None,
26+
timeout: Optional[int] = None
27+
):
28+
"""Asynchronous task to analyze a Git repository."""
29+
try:
30+
logger.info(f"[{analysis_id}] Starting Git analysis for {git_url}")
31+
analysis_results[analysis_id]["status"] = "running"
32+
33+
result = await sast_manager.scan_codebase(
34+
codebase_path=git_url,
35+
config_paths=config_paths,
36+
timeout=timeout,
37+
branch=branch or "main"
38+
)
39+
40+
analysis_results[analysis_id] = {
41+
"analysis_id": analysis_id,
42+
"status": "completed",
43+
"results": result
44+
}
45+
46+
except Exception as e:
47+
logger.error(f"[{analysis_id}] Git analysis failed: {str(e)}")
48+
analysis_results[analysis_id] = {
49+
"status": "failed",
50+
"error": str(e)
51+
}
52+
53+
54+
async def _run_file_analysis(
55+
analysis_id: str,
56+
file_paths: List[str],
57+
config_paths: Optional[Dict[str, str]] = None,
58+
timeout: Optional[int] = None,
59+
original_filenames: Optional[List[str]] = None
60+
):
61+
"""Asynchronous task to analyze uploaded files."""
62+
try:
63+
logger.info(f"[{analysis_id}] Starting file analysis")
64+
analysis_results[analysis_id]["status"] = "running"
65+
logger.info(f"[{analysis_id}] File paths: {file_paths}")
66+
result = await sast_manager.scan_codebase(
67+
codebase_path=file_paths,
68+
config_paths=config_paths,
69+
timeout=timeout,
70+
original_path=original_filenames
71+
)
72+
73+
analysis_results[analysis_id] = {
74+
"analysis_id": analysis_id,
75+
"status": "completed",
76+
"results": result
77+
}
78+
79+
except Exception as e:
80+
logger.error(f"[{analysis_id}] File analysis failed: {str(e)}")
81+
analysis_results[analysis_id] = {
82+
"status": "failed",
83+
"error": str(e)
84+
}
85+
86+
87+
@analyze_router.post("/git", response_model=AnalysisResponse)
88+
async def analyze_git_repo(
89+
background_tasks: BackgroundTasks,
90+
repo_request: GitRepoRequest,
91+
):
92+
"""Initiate analysis on a Git repository."""
93+
try:
94+
analysis_id = str(uuid4())
95+
analysis_results[analysis_id] = {"status": "pending"}
96+
97+
background_tasks.add_task(
98+
_run_git_analysis,
99+
analysis_id,
100+
repo_request.git_url,
101+
repo_request.branch,
102+
repo_request.config_paths,
103+
repo_request.timeout
104+
)
105+
106+
return AnalysisResponse(analysis_id=analysis_id, status="pending")
107+
108+
except Exception as e:
109+
logger.error(f"Error initiating Git analysis: {str(e)}")
110+
raise HTTPException(status_code=500, detail=str(e))
111+
112+
113+
@analyze_router.post("/files", response_model=AnalysisResponse)
114+
async def analyze_files(
115+
background_tasks: BackgroundTasks,
116+
files: List[UploadFile] = File(...),
117+
config_paths: Optional[str] = Form(None),
118+
timeout: Optional[int] = Form(None),
119+
):
120+
"""Initiate analysis on uploaded file archives."""
121+
try:
122+
logger.info(f"Received {len(files)} files for analysis")
123+
analysis_id = str(uuid4())
124+
analysis_results[analysis_id] = {"status": "pending"}
125+
126+
config_dict = {}
127+
if config_paths:
128+
try:
129+
config_dict = json.loads(config_paths)
130+
except json.JSONDecodeError:
131+
raise HTTPException(status_code=400, detail="Invalid config_paths JSON format")
132+
133+
temp_file_paths = []
134+
original_filenames = []
135+
136+
for upload_file in files:
137+
if not any(upload_file.filename.endswith(ext) for ext in ['.zip', '.tar', '.gz']):
138+
raise HTTPException(
139+
status_code=400,
140+
detail=f"Unsupported file format: {upload_file.filename}"
141+
)
142+
143+
with NamedTemporaryFile(delete=False, suffix=os.path.splitext(upload_file.filename)[1]) as temp_file:
144+
temp_file.write(await upload_file.read())
145+
temp_file_paths.append(temp_file.name)
146+
original_filenames.append(upload_file.filename)
147+
logger.info(upload_file.filename)
148+
149+
background_tasks.add_task(
150+
_run_file_analysis,
151+
analysis_id,
152+
temp_file_paths[0],
153+
config_dict,
154+
timeout,
155+
original_filenames[0]
156+
)
157+
158+
return AnalysisResponse(analysis_id=analysis_id, status="pending")
159+
160+
except Exception as e:
161+
logger.error(f"Error initiating file analysis: {str(e)}")
162+
raise HTTPException(status_code=500, detail=str(e))
163+
164+
165+
@analyze_router.get("/status/{analysis_id}", response_model=AnalysisResponse)
166+
async def get_analysis_status(analysis_id: str):
167+
if analysis_id not in analysis_results:
168+
raise HTTPException(status_code=404, detail="Analysis ID not found")
169+
170+
return AnalysisResponse(analysis_id=analysis_id, status=analysis_results[analysis_id]["status"])
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import io
2+
import json
3+
from datetime import datetime
4+
from typing import Literal
5+
6+
from analyticq.service.scan_service import (AnalyticQScanResultRepository,
7+
AnalyticQScanService)
8+
from fastapi import APIRouter, Depends, Query, Response
9+
from fastapi.responses import HTMLResponse, StreamingResponse
10+
11+
report_router = APIRouter(prefix="/reports", tags=["reports"])
12+
13+
14+
def get_scan_service(
15+
repo: AnalyticQScanResultRepository = Depends(AnalyticQScanResultRepository)
16+
) -> AnalyticQScanService:
17+
"""Dependency provider for scan service."""
18+
return AnalyticQScanService(repo)
19+
20+
21+
@report_router.get("/download/{scan_id}")
22+
async def download_report(
23+
scan_id: str,
24+
format: Literal["pdf", "json", "html", "csv"] = Query(..., description="Report format: pdf, json, html, or csv"),
25+
scan_service: AnalyticQScanService = Depends(get_scan_service)
26+
):
27+
content = await scan_service.get_scan_report(scan_id, format)
28+
report_filename = f"analyticq_report_{scan_id}_{datetime.now().strftime('%Y%m%d')}".lower()
29+
30+
if format == "json":
31+
return Response(
32+
content=json.dumps(content, indent=2),
33+
media_type="application/json",
34+
headers={"Content-Disposition": f"attachment; filename={report_filename}.json"}
35+
)
36+
elif format == "html":
37+
return HTMLResponse(
38+
content=content,
39+
headers={"Content-Disposition": f"attachment; filename={report_filename}.html"}
40+
)
41+
elif format == "pdf":
42+
return StreamingResponse(
43+
io.BytesIO(content),
44+
media_type="application/pdf",
45+
headers={"Content-Disposition": f"attachment; filename={report_filename}.pdf"}
46+
)
47+
elif format == "csv":
48+
return Response(
49+
content=content,
50+
media_type="text/csv",
51+
headers={"Content-Disposition": f"attachment; filename={report_filename}.csv"}
52+
)

analyticq-backend/analyticq/app.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
from contextlib import asynccontextmanager
88
from typing import List
99

10+
from .jinja import setup_jinja
11+
1012
if platform.system() == "Windows":
1113
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
1214

1315
from analyticq.celery_app import get_celery_app
1416
from analyticq.config.di import AnalyticQContainer
1517
from analyticq.engine.core import AnalyticQToolDiscoverer
1618
from analyticq.manager import AnalyticQDatabaseManager, DockerImageManager
17-
from analyticq.manager.tool_manager import AnalyticQSASTManager
1819
from analyticq.util import AnalyticQConst, PathUtil
1920
from fastapi import FastAPI
2021
from fastapi.middleware.cors import CORSMiddleware
@@ -165,14 +166,15 @@ async def backend_context(app: FastAPI):
165166
celery_app = get_celery_app()
166167
app.state.celery = celery_app
167168

169+
logger.info("Init Jinja for templating engine...")
170+
setup_jinja(app)
171+
168172
logger.info("Checking root exsistence for scanning codebase")
169173
await create_AnalyticQ_root_structure()
170174

171175
logger.info("Initializing AnalyticQ SAST Tool Registry...")
172176
AnalyticQToolDiscoverer.discover_and_register_tools()
173177

174-
analyticq_mangaer = AnalyticQSASTManager()
175-
await analyticq_mangaer.scan_codebase("https://github.com/paulc4/microservices-demo")
176178
logger.info(f"Analyticq backend started in {time.time() - start_time:2f} seconds")
177179

178180
yield
@@ -234,13 +236,16 @@ def set_app_routes(app: FastAPI) -> None :
234236
- tool_router: Handles tool-related endpoints
235237
- issue_router: Handles issue-related endpoints
236238
"""
237-
from analyticq.api import contexts, issues, scans, stats, tools
239+
from analyticq.api import (analyze, contexts, issues, report, scans, stats,
240+
tools)
238241

239242
app.include_router(stats.stats_router)
240243
app.include_router(contexts.context_router)
241244
app.include_router(scans.scan_router)
242245
app.include_router(tools.tool_router)
243246
app.include_router(issues.issue_router)
247+
app.include_router(report.report_router)
248+
app.include_router(analyze.analyze_router)
244249

245250

246251
def create_app(config_file: str = AnalyticQConst.ANALYTICQ_DEFAULT_CONFIG_FILE,

analyticq-backend/analyticq/engine/core/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class AnalyticQSeverity(Enum):
3434
Enumeration representing severity levels for AnalyticQ issues.
3535
3636
Attributes:
37+
INFO (str): INFO severity level
3738
HIGH (str): Critical severity level
3839
MEDIUM (str): Moderate severity level
3940
LOW (str): Minor severity level
@@ -44,6 +45,7 @@ class AnalyticQSeverity(Enum):
4445
MEDIUM = "MEDIUM"
4546
LOW = "LOW"
4647
CRITICAL = "CRITICAL"
48+
WARNING = "WARNING" # Not all SAST tool support this severity lev
4749
UNKNOWN = "UNKNOWN"
4850

4951
@classmethod

analyticq-backend/analyticq/engine/core/result_parser.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,28 @@ def map_endline(self, end_line: Union[List, str]):
172172
return end_line if end_line else 0
173173

174174
def _process_metadata(self, raw_issue: Dict[str, Any]) -> Dict[str, Any]:
175+
"""
176+
Processes and extracts metadata from a raw issue dictionary based on configured field mapping.
177+
178+
This method traverses the raw issue dictionary using dot notation specified in the field mapping
179+
to locate and extract metadata. It handles different data types (dict, list, simple values)
180+
and processes them accordingly.
181+
182+
Args:
183+
raw_issue (Dict[str, Any]): The raw issue dictionary containing metadata information.
184+
185+
Returns:
186+
Dict[str, Any]: Processed metadata dictionary. Returns:
187+
- Copy of all fields if metadata is a dictionary
188+
- Dictionary with "items" key if metadata is a list
189+
- Dictionary with "value" key if metadata is a simple value
190+
- Empty dictionary if metadata field is not found or on error
191+
192+
Example:
193+
If field_mapping["issue_metadata"] = "metadata.fields"
194+
raw_issue = {"metadata": {"fields": {"key1": "value1", "key2": "value2"}}}
195+
Returns: {"key1": "value1", "key2": "value2"}
196+
"""
175197

176198
metadata_field = self.field_mapping.get("issue_metadata")
177199
if not metadata_field:
@@ -224,6 +246,8 @@ def parse_scan_result(self, raw_result: Dict[str, Any]) -> AnalyticQSASTScanResu
224246
summary = self._generate_summary(issues)
225247
metadata = self._generate_metadata()
226248

249+
print(issues)
250+
227251
return AnalyticQSASTScanResultModel(
228252
scan_id=new_scan_id,
229253
issues=issues,

analyticq-backend/analyticq/engine/parser/js/eslint_parser.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@ def __init__(self):
1919
"column": "column",
2020
"end_column": "endColumn",
2121
"severity": "severity",
22-
"node_type": "nodeType",
2322
"code": "source",
24-
"issue_metadata": "metadata"
23+
"issue_metadata": "issue_metadata"
2524
}
2625
super().__init__(tool_name="eslint", field_mapping=field_mapping)
2726

@@ -51,23 +50,26 @@ def transform_output(self, raw_result: List[Dict[str, Any]]) -> List[Dict[str, A
5150
source_code = file_data.get("source", "")
5251

5352
for message in file_data.get("messages", []):
53+
rule_id = message.get("ruleId")
54+
if not isinstance(rule_id, str):
55+
rule_id = str(rule_id) if rule_id is not None else "unknown"
56+
5457
transformed_issue = {
55-
"ruleId": message.get("ruleId", "unknown"),
58+
"ruleId": rule_id,
5659
"filePath": file_path,
5760
"message": message.get("message", "unknown"),
5861
"severity": message.get("severity", 0),
5962
"line": message.get("line", 0),
6063
"endLine": message.get("endLine", message.get("line", 0)),
6164
"column": message.get("column", 0),
6265
"endColumn": message.get("endColumn", message.get("column", 0)),
63-
"nodeType": message.get("nodeType", "unknown"),
6466
"source": source_code
6567
}
6668

67-
# Add any suggestions as metadata
68-
if "suggestions" in message:
69-
transformed_issue["metadata"] = {
70-
"suggestions": message.get("suggestions", [])
69+
suggestions = message.get("suggestions")
70+
if isinstance(suggestions, list) and suggestions:
71+
transformed_issue["issue_metadata"] = {
72+
"suggestions": suggestions
7173
}
7274

7375
transformed_issues.append(transformed_issue)

analyticq-backend/analyticq/engine/parser/js/njsscan_parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class NjsScanParser(AnalyticQResultParser):
1111

1212
def __init__(self):
1313
field_mapping = {
14-
"ruele_id": "rule_id",
14+
"rule_id": "rule_id",
1515
"message": "description",
1616
"path": "file_path",
1717
"start_line": "start_line",

analyticq-backend/analyticq/engine/parser/python/pylint_parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def __init__(self):
2222
}
2323

2424
self.severity_mapping = {
25+
"info": "INFO",
2526
"convention": "LOW",
2627
"refactor": "LOW",
2728
"warning": "MEDIUM",

0 commit comments

Comments
 (0)