dap-platform 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dap_platform-0.1.0/.github/workflows/publish.yml +21 -0
- dap_platform-0.1.0/.gitignore +19 -0
- dap_platform-0.1.0/LICENSE +21 -0
- dap_platform-0.1.0/PKG-INFO +31 -0
- dap_platform-0.1.0/README.md +5 -0
- dap_platform-0.1.0/pyproject.toml +33 -0
- dap_platform-0.1.0/rename.py +23 -0
- dap_platform-0.1.0/rename_pkg.py +9 -0
- dap_platform-0.1.0/src/dataaudit/__init__.py +2 -0
- dap_platform-0.1.0/src/dataaudit/__main__.py +3 -0
- dap_platform-0.1.0/src/dataaudit/app.py +473 -0
- dap_platform-0.1.0/src/dataaudit/auth/__init__.py +20 -0
- dap_platform-0.1.0/src/dataaudit/auth/dependencies.py +80 -0
- dap_platform-0.1.0/src/dataaudit/auth/local_auth.py +40 -0
- dap_platform-0.1.0/src/dataaudit/auth/models.py +94 -0
- dap_platform-0.1.0/src/dataaudit/auth/session.py +95 -0
- dap_platform-0.1.0/src/dataaudit/cli.py +80 -0
- dap_platform-0.1.0/src/dataaudit/config/useful_links.yaml +58 -0
- dap_platform-0.1.0/src/dataaudit/database.py +230 -0
- dap_platform-0.1.0/src/dataaudit/routers/__init__.py +1 -0
- dap_platform-0.1.0/src/dataaudit/routers/bi_native.py +688 -0
- dap_platform-0.1.0/src/dataaudit/routers/grc.py +3068 -0
- dap_platform-0.1.0/src/dataaudit/routers/my_reports.py +140 -0
- dap_platform-0.1.0/src/dataaudit/routers/workflows.py +37 -0
- dap_platform-0.1.0/src/dataaudit/schema.py +1078 -0
- dap_platform-0.1.0/src/dataaudit/services/__init__.py +1 -0
- dap_platform-0.1.0/src/dataaudit/services/docgen.py +421 -0
- dap_platform-0.1.0/src/dataaudit/services/email.py +87 -0
- dap_platform-0.1.0/src/dataaudit/static/css/bi.css +967 -0
- dap_platform-0.1.0/src/dataaudit/static/css/grc.css +1434 -0
- dap_platform-0.1.0/src/dataaudit/static/css/style.css +989 -0
- dap_platform-0.1.0/src/dataaudit/static/images/favicon.png +0 -0
- dap_platform-0.1.0/src/dataaudit/static/images/favicon_old.png +0 -0
- dap_platform-0.1.0/src/dataaudit/static/images/logo.png +0 -0
- dap_platform-0.1.0/src/dataaudit/static/images/logo_old.png +0 -0
- dap_platform-0.1.0/src/dataaudit/static/js/app.js +11 -0
- dap_platform-0.1.0/src/dataaudit/static/js/bi.js +696 -0
- dap_platform-0.1.0/src/dataaudit/static/js/grc.js +422 -0
- dap_platform-0.1.0/src/dataaudit/templates/base.html +124 -0
- dap_platform-0.1.0/src/dataaudit/templates/login.html +68 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/ai_assistant.html +15 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/bi/chart_edit.html +642 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/bi/charts.html +88 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/bi/dashboard_view.html +198 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/bi/dashboards.html +134 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/bi/datasources.html +243 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/bi/sql_lab.html +395 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/bi_analytics.html +167 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/data_governance.html +15 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/grc/engagement_detail.html +525 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/grc/engagements.html +106 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/grc/finding_detail.html +394 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/grc/notifications.html +97 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/grc/plan_detail.html +381 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/grc/plans.html +178 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/grc/remediation_detail.html +381 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/grc/remediations.html +183 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/grc/settings_workflows.html +772 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/grc/strategic_plan_detail.html +400 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/grc/strategic_plans.html +168 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/grc/universe.html +729 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/home.html +59 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/my_reports.html +562 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/reports.html +15 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/settings.html +15 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/useful_links.html +139 -0
- dap_platform-0.1.0/src/dataaudit/templates/pages/workflows.html +322 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: '3.12'
|
|
19
|
+
- run: pip install build
|
|
20
|
+
- run: python -m build
|
|
21
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pavel Maximov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dap-platform
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Audit management platform with GRC workflows and BI analytics
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: fastapi>=0.104
|
|
9
|
+
Requires-Dist: httpx>=0.25
|
|
10
|
+
Requires-Dist: itsdangerous>=2.0
|
|
11
|
+
Requires-Dist: jinja2>=3.1
|
|
12
|
+
Requires-Dist: python-docx>=1.0
|
|
13
|
+
Requires-Dist: python-multipart>=0.0.6
|
|
14
|
+
Requires-Dist: pyyaml>=6.0
|
|
15
|
+
Requires-Dist: uvicorn[standard]>=0.24
|
|
16
|
+
Provides-Extra: duckdb
|
|
17
|
+
Requires-Dist: duckdb>=0.9; extra == 'duckdb'
|
|
18
|
+
Provides-Extra: ldap
|
|
19
|
+
Requires-Dist: ldap3>=2.9; extra == 'ldap'
|
|
20
|
+
Provides-Extra: oracle
|
|
21
|
+
Requires-Dist: oracledb>=2.0; extra == 'oracle'
|
|
22
|
+
Provides-Extra: postgres
|
|
23
|
+
Requires-Dist: asyncpg>=0.29; extra == 'postgres'
|
|
24
|
+
Requires-Dist: psycopg2-binary>=2.9; extra == 'postgres'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# DataAudit Platform
|
|
28
|
+
Audit management platform with GRC workflows and BI analytics.
|
|
29
|
+
|
|
30
|
+
Install: pip install dap-platform
|
|
31
|
+
Run: dataaudit serve
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dap-platform"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Audit management platform with GRC workflows and BI analytics"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"fastapi>=0.104",
|
|
14
|
+
"uvicorn[standard]>=0.24",
|
|
15
|
+
"jinja2>=3.1",
|
|
16
|
+
"python-multipart>=0.0.6",
|
|
17
|
+
"itsdangerous>=2.0",
|
|
18
|
+
"httpx>=0.25",
|
|
19
|
+
"pyyaml>=6.0",
|
|
20
|
+
"python-docx>=1.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
postgres = ["asyncpg>=0.29", "psycopg2-binary>=2.9"]
|
|
25
|
+
ldap = ["ldap3>=2.9"]
|
|
26
|
+
oracle = ["oracledb>=2.0"]
|
|
27
|
+
duckdb = ["duckdb>=0.9"]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
dataaudit = "dataaudit.cli:main"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["src/dataaudit"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
# 1. pyproject.toml
|
|
4
|
+
pp = pathlib.Path("pyproject.toml")
|
|
5
|
+
txt = pp.read_text(encoding="utf-8")
|
|
6
|
+
txt = txt.replace('name = "dataaudit"', 'name = "dap-platform"', 1)
|
|
7
|
+
pp.write_text(txt, encoding="utf-8")
|
|
8
|
+
print("pyproject.toml: name -> dap-platform")
|
|
9
|
+
|
|
10
|
+
# 2. README.md - update install commands
|
|
11
|
+
rm = pathlib.Path("README.md")
|
|
12
|
+
if rm.exists():
|
|
13
|
+
rtxt = rm.read_text(encoding="utf-8")
|
|
14
|
+
rtxt = rtxt.replace("pip install dataaudit", "pip install dap-platform")
|
|
15
|
+
rtxt = rtxt.replace("pypi.org/project/dataaudit", "pypi.org/project/dap-platform")
|
|
16
|
+
rtxt = rtxt.replace("badge.fury.io/py/dataaudit", "badge.fury.io/py/dap-platform")
|
|
17
|
+
rm.write_text(rtxt, encoding="utf-8")
|
|
18
|
+
print("README.md: updated")
|
|
19
|
+
|
|
20
|
+
# 3. Rebuild
|
|
21
|
+
print("\nNow run:")
|
|
22
|
+
print(" Remove-Item dist/* -Force")
|
|
23
|
+
print(" python -m build")
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
from fastapi import FastAPI, Request, Form
|
|
2
|
+
from fastapi.staticfiles import StaticFiles
|
|
3
|
+
from fastapi.templating import Jinja2Templates
|
|
4
|
+
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import asyncio
|
|
7
|
+
import os
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime, time as dtime, timedelta
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from dataaudit.routers.bi_native import router as bi_router
|
|
13
|
+
from dataaudit.routers.workflows import router as workflows_router
|
|
14
|
+
from dataaudit.routers.grc import router as grc_router
|
|
15
|
+
from dataaudit.routers.my_reports import router as my_reports_router
|
|
16
|
+
from dataaudit.auth import (
|
|
17
|
+
get_current_user,
|
|
18
|
+
auth_middleware,
|
|
19
|
+
add_user_to_context,
|
|
20
|
+
)
|
|
21
|
+
from dataaudit.auth.local_auth import get_auth_client
|
|
22
|
+
from dataaudit.auth.session import get_session_manager
|
|
23
|
+
logging.getLogger("dap.scheduler").setLevel(logging.INFO)
|
|
24
|
+
logging.getLogger("dap.scheduler").addHandler(logging.StreamHandler())
|
|
25
|
+
app = FastAPI(title="DataAudit Platform")
|
|
26
|
+
app.include_router(bi_router)
|
|
27
|
+
app.include_router(workflows_router)
|
|
28
|
+
app.include_router(grc_router)
|
|
29
|
+
app.include_router(my_reports_router)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Auth middleware - protects all routes except /login, /health, /static
|
|
33
|
+
app.middleware("http")(auth_middleware)
|
|
34
|
+
|
|
35
|
+
# Mount static files
|
|
36
|
+
_PKG_DIR = Path(__file__).parent
|
|
37
|
+
app.mount("/static", StaticFiles(directory=str(_PKG_DIR / "static")), name="static")
|
|
38
|
+
|
|
39
|
+
# Setup Jinja2 templates
|
|
40
|
+
templates = Jinja2Templates(directory=str(_PKG_DIR / "templates"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_useful_links():
|
|
44
|
+
"""Load useful links from YAML config"""
|
|
45
|
+
config_path = _PKG_DIR / "config" / "useful_links.yaml"
|
|
46
|
+
try:
|
|
47
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
48
|
+
return yaml.safe_load(f)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
print(f"Error loading useful links: {e}")
|
|
51
|
+
return {"categories": []}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.get("/", response_class=HTMLResponse)
|
|
55
|
+
async def home(request: Request):
|
|
56
|
+
return templates.TemplateResponse(
|
|
57
|
+
"pages/home.html",
|
|
58
|
+
{"request": request, "current_page": "home", **add_user_to_context(request)}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.get("/reports", response_class=HTMLResponse)
|
|
63
|
+
async def reports(request: Request):
|
|
64
|
+
"""Legacy route - redirect to my-reports"""
|
|
65
|
+
return RedirectResponse(url="/my-reports", status_code=302)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.get("/my-reports", response_class=HTMLResponse)
|
|
69
|
+
async def my_reports_page(request: Request):
|
|
70
|
+
return templates.TemplateResponse(
|
|
71
|
+
"pages/my_reports.html",
|
|
72
|
+
{"request": request, "current_page": "my-reports", **add_user_to_context(request)}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ── BI Analytics (Native) ─────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
@app.get("/analytics", response_class=HTMLResponse)
|
|
79
|
+
async def analytics(request: Request):
|
|
80
|
+
return templates.TemplateResponse(
|
|
81
|
+
"pages/bi_analytics.html",
|
|
82
|
+
{"request": request, "current_page": "analytics", **add_user_to_context(request)}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.get("/analytics/sql", response_class=HTMLResponse)
|
|
87
|
+
async def analytics_sql(request: Request):
|
|
88
|
+
return templates.TemplateResponse(
|
|
89
|
+
"pages/bi/sql_lab.html",
|
|
90
|
+
{"request": request, "current_page": "analytics", **add_user_to_context(request)}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@app.get("/analytics/charts", response_class=HTMLResponse)
|
|
95
|
+
async def analytics_charts(request: Request):
|
|
96
|
+
return templates.TemplateResponse(
|
|
97
|
+
"pages/bi/charts.html",
|
|
98
|
+
{"request": request, "current_page": "analytics", **add_user_to_context(request)}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@app.get("/analytics/charts/{chart_id}/edit", response_class=HTMLResponse)
|
|
103
|
+
async def analytics_chart_edit(request: Request, chart_id: int):
|
|
104
|
+
return templates.TemplateResponse(
|
|
105
|
+
"pages/bi/chart_edit.html",
|
|
106
|
+
{"request": request, "current_page": "analytics", "chart_id": chart_id, **add_user_to_context(request)}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@app.get("/analytics/dashboards", response_class=HTMLResponse)
|
|
111
|
+
async def analytics_dashboards(request: Request):
|
|
112
|
+
return templates.TemplateResponse(
|
|
113
|
+
"pages/bi/dashboards.html",
|
|
114
|
+
{"request": request, "current_page": "analytics", **add_user_to_context(request)}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.get("/analytics/dashboards/{dashboard_id}", response_class=HTMLResponse)
|
|
119
|
+
async def analytics_dashboard_view(request: Request, dashboard_id: int):
|
|
120
|
+
return templates.TemplateResponse(
|
|
121
|
+
"pages/bi/dashboard_view.html",
|
|
122
|
+
{"request": request, "current_page": "analytics", "dashboard_id": dashboard_id, **add_user_to_context(request)}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.get("/analytics/datasources", response_class=HTMLResponse)
|
|
127
|
+
async def analytics_datasources(request: Request):
|
|
128
|
+
return templates.TemplateResponse(
|
|
129
|
+
"pages/bi/datasources.html",
|
|
130
|
+
{"request": request, "current_page": "analytics", **add_user_to_context(request)}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ── GRC Workflows (Native) ────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
@app.get("/workflows", response_class=HTMLResponse)
|
|
137
|
+
async def workflows(request: Request):
|
|
138
|
+
return templates.TemplateResponse(
|
|
139
|
+
"pages/workflows.html",
|
|
140
|
+
{"request": request, "current_page": "workflows", **add_user_to_context(request)}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@app.get("/workflows/plans", response_class=HTMLResponse)
|
|
145
|
+
async def grc_plans(request: Request):
|
|
146
|
+
return templates.TemplateResponse(
|
|
147
|
+
"pages/grc/plans.html",
|
|
148
|
+
{"request": request, "current_page": "workflows", **add_user_to_context(request)}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@app.get("/workflows/plans/{plan_id}", response_class=HTMLResponse)
|
|
153
|
+
async def grc_plan_detail(request: Request, plan_id: int):
|
|
154
|
+
return templates.TemplateResponse(
|
|
155
|
+
"pages/grc/plan_detail.html",
|
|
156
|
+
{"request": request, "current_page": "workflows", "plan_id": plan_id, **add_user_to_context(request)}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@app.get("/workflows/engagements", response_class=HTMLResponse)
|
|
161
|
+
async def grc_engagements(request: Request):
|
|
162
|
+
return templates.TemplateResponse(
|
|
163
|
+
"pages/grc/engagements.html",
|
|
164
|
+
{"request": request, "current_page": "workflows", **add_user_to_context(request)}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@app.get("/workflows/engagements/{engagement_id}", response_class=HTMLResponse)
|
|
169
|
+
async def grc_engagement_detail(request: Request, engagement_id: int):
|
|
170
|
+
return templates.TemplateResponse(
|
|
171
|
+
"pages/grc/engagement_detail.html",
|
|
172
|
+
{"request": request, "current_page": "workflows", "engagement_id": engagement_id, **add_user_to_context(request)}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@app.get("/workflows/findings/{finding_id}", response_class=HTMLResponse)
|
|
177
|
+
async def grc_finding_detail(request: Request, finding_id: int):
|
|
178
|
+
return templates.TemplateResponse(
|
|
179
|
+
"pages/grc/finding_detail.html",
|
|
180
|
+
{"request": request, "current_page": "workflows", "finding_id": finding_id, **add_user_to_context(request)}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@app.get("/workflows/remediations", response_class=HTMLResponse)
|
|
185
|
+
async def grc_remediations(request: Request):
|
|
186
|
+
return templates.TemplateResponse(
|
|
187
|
+
"pages/grc/remediations.html",
|
|
188
|
+
{"request": request, "current_page": "workflows", **add_user_to_context(request)}
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@app.get("/workflows/remediations/{remediation_id}", response_class=HTMLResponse)
|
|
193
|
+
async def grc_remediation_detail(request: Request, remediation_id: int):
|
|
194
|
+
return templates.TemplateResponse(
|
|
195
|
+
"pages/grc/remediation_detail.html",
|
|
196
|
+
{"request": request, "current_page": "workflows", "remediation_id": remediation_id, **add_user_to_context(request)}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@app.get("/workflows/notifications", response_class=HTMLResponse)
|
|
201
|
+
async def grc_notifications(request: Request):
|
|
202
|
+
return templates.TemplateResponse(
|
|
203
|
+
"pages/grc/notifications.html",
|
|
204
|
+
{"request": request, "current_page": "workflows", **add_user_to_context(request)}
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@app.get("/workflows/settings", response_class=HTMLResponse)
|
|
209
|
+
async def grc_settings(request: Request):
|
|
210
|
+
return templates.TemplateResponse(
|
|
211
|
+
"pages/grc/settings_workflows.html",
|
|
212
|
+
{"request": request, "current_page": "workflows", **add_user_to_context(request)}
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
@app.get("/workflows/universe", response_class=HTMLResponse)
|
|
216
|
+
async def page_universe(request: Request):
|
|
217
|
+
return templates.TemplateResponse(
|
|
218
|
+
"pages/grc/universe.html",
|
|
219
|
+
{"request": request, "current_page": "workflows", **add_user_to_context(request)}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
@app.get("/workflows/strategic-plans", response_class=HTMLResponse)
|
|
223
|
+
async def page_strategic_plans(request: Request):
|
|
224
|
+
return templates.TemplateResponse(
|
|
225
|
+
"pages/grc/strategic_plans.html",
|
|
226
|
+
{"request": request, "current_page": "workflows", **add_user_to_context(request)}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
@app.get("/workflows/strategic-plans/{plan_id}", response_class=HTMLResponse)
|
|
230
|
+
async def page_strategic_plan_detail(request: Request, plan_id: int):
|
|
231
|
+
return templates.TemplateResponse(
|
|
232
|
+
"pages/grc/strategic_plan_detail.html",
|
|
233
|
+
{"request": request, "current_page": "workflows", "plan_id": plan_id, **add_user_to_context(request)}
|
|
234
|
+
)
|
|
235
|
+
# ── Other Pages ────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
@app.get("/ai", response_class=HTMLResponse)
|
|
238
|
+
async def ai_assistant(request: Request):
|
|
239
|
+
return templates.TemplateResponse(
|
|
240
|
+
"pages/ai_assistant.html",
|
|
241
|
+
{"request": request, "current_page": "ai", **add_user_to_context(request)}
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@app.get("/governance", response_class=HTMLResponse)
|
|
246
|
+
async def data_governance(request: Request):
|
|
247
|
+
return templates.TemplateResponse(
|
|
248
|
+
"pages/data_governance.html",
|
|
249
|
+
{"request": request, "current_page": "governance", **add_user_to_context(request)}
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@app.get("/useful-links", response_class=HTMLResponse)
|
|
254
|
+
async def useful_links(request: Request):
|
|
255
|
+
links_config = load_useful_links()
|
|
256
|
+
return templates.TemplateResponse(
|
|
257
|
+
"pages/useful_links.html",
|
|
258
|
+
{
|
|
259
|
+
"request": request,
|
|
260
|
+
"current_page": "useful-links",
|
|
261
|
+
"categories": links_config.get("categories", []),
|
|
262
|
+
**add_user_to_context(request)
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@app.get("/settings", response_class=HTMLResponse)
|
|
268
|
+
async def settings(request: Request):
|
|
269
|
+
return templates.TemplateResponse(
|
|
270
|
+
"pages/settings.html",
|
|
271
|
+
{"request": request, "current_page": "settings", **add_user_to_context(request)}
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@app.get("/login", response_class=HTMLResponse)
|
|
276
|
+
async def login(request: Request, next: str = "/", error: str = None):
|
|
277
|
+
# If already logged in, redirect
|
|
278
|
+
user = await get_current_user(request)
|
|
279
|
+
if user:
|
|
280
|
+
return RedirectResponse(url=next, status_code=302)
|
|
281
|
+
|
|
282
|
+
error_messages = {
|
|
283
|
+
"invalid": "Неверный логин или пароль",
|
|
284
|
+
"expired": "Сессия истекла. Войдите снова.",
|
|
285
|
+
"unavailable": "Сервис авторизации недоступен. Обратитесь в службу поддержки.",
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return templates.TemplateResponse(
|
|
289
|
+
"login.html",
|
|
290
|
+
{"request": request, "next": next, "error_message": error_messages.get(error)}
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@app.post("/login")
|
|
295
|
+
async def login_post(
|
|
296
|
+
request: Request,
|
|
297
|
+
username: str = Form(...),
|
|
298
|
+
password: str = Form(...),
|
|
299
|
+
next: str = Form("/"),
|
|
300
|
+
):
|
|
301
|
+
auth = get_auth_client()
|
|
302
|
+
session_mgr = get_session_manager()
|
|
303
|
+
|
|
304
|
+
result = await auth.authenticate(username.strip(), password)
|
|
305
|
+
|
|
306
|
+
if not result.success:
|
|
307
|
+
error = "unavailable" if result.error_code == "LDAP_UNREACHABLE" else "invalid"
|
|
308
|
+
return RedirectResponse(url=f"/login?error={error}&next={next}", status_code=302)
|
|
309
|
+
|
|
310
|
+
session = session_mgr.create_session(result.user)
|
|
311
|
+
response = RedirectResponse(url=next, status_code=302)
|
|
312
|
+
session_mgr.set_session_cookie(response, session)
|
|
313
|
+
return response
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@app.get("/logout")
|
|
317
|
+
async def logout(request: Request):
|
|
318
|
+
session_mgr = get_session_manager()
|
|
319
|
+
html = """
|
|
320
|
+
<!DOCTYPE html>
|
|
321
|
+
<html>
|
|
322
|
+
<head><title>Выход...</title></head>
|
|
323
|
+
<body>
|
|
324
|
+
<script>
|
|
325
|
+
window.location.replace('/login');
|
|
326
|
+
</script>
|
|
327
|
+
</body>
|
|
328
|
+
</html>
|
|
329
|
+
"""
|
|
330
|
+
response = HTMLResponse(html)
|
|
331
|
+
session_mgr.logout(request, response)
|
|
332
|
+
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
333
|
+
return response
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@app.get("/api/me")
|
|
337
|
+
async def me(request: Request):
|
|
338
|
+
user = await get_current_user(request)
|
|
339
|
+
if not user:
|
|
340
|
+
return {"error": "Не авторизован"}
|
|
341
|
+
return user.to_dict()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ── Escalation Scheduler ─────────────────────────────────────────────
|
|
345
|
+
_sched_logger = logging.getLogger("dap.scheduler")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
async def _run_escalation():
|
|
349
|
+
"""Direct escalation check — no HTTP, no auth needed."""
|
|
350
|
+
from dataaudit.database import get_pool
|
|
351
|
+
from dataaudit.routers.grc import (
|
|
352
|
+
_get_config, _notify, _resolve_user_email,
|
|
353
|
+
_escalation_email_html, get_email_service, _today,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
pool = await get_pool()
|
|
357
|
+
today = _today()
|
|
358
|
+
|
|
359
|
+
escalation_config = await _get_config(pool, "escalation", "deadlines")
|
|
360
|
+
thresholds = escalation_config.get("thresholds", [])
|
|
361
|
+
|
|
362
|
+
tasks = await pool.fetch(
|
|
363
|
+
"""SELECT r.*, f.title as finding_title, f.ref_number as finding_ref,
|
|
364
|
+
u.head_user_id as unit_head
|
|
365
|
+
FROM grc.remediation_tasks r
|
|
366
|
+
LEFT JOIN grc.findings f ON r.finding_id = f.id
|
|
367
|
+
LEFT JOIN grc.org_units u ON r.assigned_to_unit_id = u.id
|
|
368
|
+
WHERE r.status NOT IN ('closed', 'cancelled', 'verified')
|
|
369
|
+
AND r.escalation_enabled = TRUE
|
|
370
|
+
AND (r.escalation_muted_until IS NULL OR r.escalation_muted_until < $1)""",
|
|
371
|
+
today,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
created = 0
|
|
375
|
+
for task in tasks:
|
|
376
|
+
deadline = task["deadline"]
|
|
377
|
+
days_until = (deadline - today).days
|
|
378
|
+
assignee = task["assigned_to_user_id"]
|
|
379
|
+
unit_head = task["unit_head"]
|
|
380
|
+
ref = task["ref_number"] or f"#{task['id']}"
|
|
381
|
+
title = task["title"]
|
|
382
|
+
|
|
383
|
+
if days_until < 0 and not task["is_overdue"]:
|
|
384
|
+
await pool.execute(
|
|
385
|
+
"UPDATE grc.remediation_tasks SET is_overdue=TRUE, updated_at=NOW() WHERE id=$1",
|
|
386
|
+
task["id"],
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
for threshold in thresholds:
|
|
390
|
+
matched = False
|
|
391
|
+
ntype = threshold.get("type", "")
|
|
392
|
+
if "days_before" in threshold:
|
|
393
|
+
db = threshold["days_before"]
|
|
394
|
+
if (db > 0 and days_until == db) or (db == 0 and days_until == 0):
|
|
395
|
+
matched = True
|
|
396
|
+
if "days_after" in threshold and days_until < 0:
|
|
397
|
+
if threshold.get("repeat") == "daily":
|
|
398
|
+
matched = True
|
|
399
|
+
elif abs(days_until) == threshold["days_after"]:
|
|
400
|
+
matched = True
|
|
401
|
+
if not matched:
|
|
402
|
+
continue
|
|
403
|
+
|
|
404
|
+
notify_targets = threshold.get("notify", [])
|
|
405
|
+
overdue_days = abs(days_until) if days_until < 0 else 0
|
|
406
|
+
msg_prefix = (
|
|
407
|
+
f"Срок через {days_until} дн." if days_until > 0
|
|
408
|
+
else "Срок сегодня" if days_until == 0
|
|
409
|
+
else f"Просрочено {overdue_days} дн."
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
people = []
|
|
413
|
+
if "assignee" in notify_targets and assignee:
|
|
414
|
+
people.append((
|
|
415
|
+
assignee,
|
|
416
|
+
f"{msg_prefix}: {ref}",
|
|
417
|
+
f"Рекомендация «{title}» — срок до {deadline}",
|
|
418
|
+
))
|
|
419
|
+
if "unit_head" in notify_targets and unit_head:
|
|
420
|
+
people.append((
|
|
421
|
+
unit_head,
|
|
422
|
+
f"{msg_prefix}: {ref} (подчинённый)",
|
|
423
|
+
f"Рекомендация «{title}» назначена {assignee} — срок до {deadline}",
|
|
424
|
+
))
|
|
425
|
+
|
|
426
|
+
for (uid, ntitle, nmessage) in people:
|
|
427
|
+
existing = await pool.fetchval(
|
|
428
|
+
"""SELECT COUNT(*) FROM grc.notifications
|
|
429
|
+
WHERE user_id=$1 AND notif_type=$2 AND entity_type='remediation'
|
|
430
|
+
AND entity_id=$3 AND created_at::date=$4""",
|
|
431
|
+
uid, ntype, task["id"], today,
|
|
432
|
+
)
|
|
433
|
+
if existing == 0:
|
|
434
|
+
await _notify(pool, uid, ntype, ntitle, nmessage, "remediation", task["id"])
|
|
435
|
+
created += 1
|
|
436
|
+
email_svc = get_email_service()
|
|
437
|
+
user_email = await _resolve_user_email(pool, uid)
|
|
438
|
+
if user_email:
|
|
439
|
+
email_svc.send(
|
|
440
|
+
to=user_email,
|
|
441
|
+
subject=f"DAP: {ntitle}",
|
|
442
|
+
body_text=f"{ntitle}\n\n{nmessage}\n\n— DataAudit Platform",
|
|
443
|
+
body_html=_escalation_email_html(ntitle, nmessage, ref, deadline),
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
_sched_logger.info(f"Escalation check: {len(tasks)} tasks, {created} notifications")
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
async def _escalation_scheduler():
|
|
450
|
+
"""Run escalation daily at 09:00."""
|
|
451
|
+
while True:
|
|
452
|
+
now = datetime.now()
|
|
453
|
+
target = datetime.combine(now.date(), dtime(9, 0))
|
|
454
|
+
if now >= target:
|
|
455
|
+
target += timedelta(days=1)
|
|
456
|
+
wait = (target - now).total_seconds()
|
|
457
|
+
_sched_logger.info(f"Next escalation: {target} (in {wait:.0f}s)")
|
|
458
|
+
await asyncio.sleep(wait)
|
|
459
|
+
try:
|
|
460
|
+
await _run_escalation()
|
|
461
|
+
except Exception as e:
|
|
462
|
+
_sched_logger.error(f"Escalation failed: {e}")
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@app.on_event("startup")
|
|
466
|
+
async def start_scheduler():
|
|
467
|
+
if os.getenv("DAP_SCHEDULER", "false").lower() == "true":
|
|
468
|
+
asyncio.create_task(_escalation_scheduler())
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
@app.get("/health")
|
|
472
|
+
async def health():
|
|
473
|
+
return {"status": "ok"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""DAP Authentication Module"""
|
|
2
|
+
|
|
3
|
+
from .models import User, Role, Session, AuthResult
|
|
4
|
+
from .local_auth import LocalAuth, get_auth_client
|
|
5
|
+
from .session import SessionManager, get_session_manager
|
|
6
|
+
from .dependencies import (
|
|
7
|
+
get_current_user,
|
|
8
|
+
require_auth,
|
|
9
|
+
require_role,
|
|
10
|
+
auth_middleware,
|
|
11
|
+
add_user_to_context,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"User", "Role", "Session", "AuthResult",
|
|
16
|
+
"LocalAuth", "get_auth_client",
|
|
17
|
+
"SessionManager", "get_session_manager",
|
|
18
|
+
"get_current_user", "require_auth", "require_role",
|
|
19
|
+
"auth_middleware", "add_user_to_context",
|
|
20
|
+
]
|