mirror of
https://framagit.org/framasoft/framaspace/argos.git
synced 2025-05-05 21:21:49 +02:00
Compare commits
6 commits
a31c12e037
...
0563cf185a
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0563cf185a | ||
![]() |
91a9b27106 | ||
![]() |
4117f9f628 | ||
![]() |
8ac2519398 | ||
![]() |
d3766a79c6 | ||
![]() |
759fa05417 |
16 changed files with 227 additions and 110 deletions
|
@ -4,6 +4,11 @@
|
||||||
|
|
||||||
- 💄 — Show only not-OK domains by default in domains list, to reduce the load on browser
|
- 💄 — Show only not-OK domains by default in domains list, to reduce the load on browser
|
||||||
- ♿️ — Fix not-OK domains display if javascript is disabled
|
- ♿️ — Fix not-OK domains display if javascript is disabled
|
||||||
|
- ✨ — Retry check right after a httpx.ReadError
|
||||||
|
- ✨ — The HTTP method used by checks is now configurable
|
||||||
|
- ♻️ — Refactor some agent code
|
||||||
|
- 💄 — Filter form on domains list (#66)
|
||||||
|
- ✨ — Add "Remember me" checkbox on login (#65)
|
||||||
|
|
||||||
## 0.5.0
|
## 0.5.0
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
|
from time import sleep
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
@ -63,9 +64,24 @@ class ArgosAgent:
|
||||||
async def _complete_task(self, _task: dict) -> AgentResult:
|
async def _complete_task(self, _task: dict) -> AgentResult:
|
||||||
try:
|
try:
|
||||||
task = Task(**_task)
|
task = Task(**_task)
|
||||||
|
|
||||||
|
url = task.url
|
||||||
|
if task.check == "http-to-https":
|
||||||
|
url = str(httpx.URL(task.url).copy_with(scheme="http"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await self._http_client.request( # type: ignore[attr-defined]
|
||||||
|
method=task.method, url=url, timeout=60
|
||||||
|
)
|
||||||
|
except httpx.ReadError:
|
||||||
|
sleep(1)
|
||||||
|
response = await self._http_client.request( # type: ignore[attr-defined]
|
||||||
|
method=task.method, url=url, timeout=60
|
||||||
|
)
|
||||||
|
|
||||||
check_class = get_registered_check(task.check)
|
check_class = get_registered_check(task.check)
|
||||||
check = check_class(self._http_client, task)
|
check = check_class(task)
|
||||||
result = await check.run()
|
result = await check.run(response)
|
||||||
status = result.status
|
status = result.status
|
||||||
context = result.context
|
context = result.context
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
import httpx
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from argos.schemas.models import Task
|
from argos.schemas.models import Task
|
||||||
|
@ -92,8 +91,7 @@ class BaseCheck:
|
||||||
raise CheckNotFound(name)
|
raise CheckNotFound(name)
|
||||||
return check
|
return check
|
||||||
|
|
||||||
def __init__(self, http_client: httpx.AsyncClient, task: Task):
|
def __init__(self, task: Task):
|
||||||
self.http_client = http_client
|
|
||||||
self.task = task
|
self.task = task
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -4,7 +4,7 @@ import json
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from httpx import URL
|
from httpx import Response
|
||||||
from jsonpointer import resolve_pointer, JsonPointerException
|
from jsonpointer import resolve_pointer, JsonPointerException
|
||||||
|
|
||||||
from argos.checks.base import (
|
from argos.checks.base import (
|
||||||
|
@ -22,13 +22,7 @@ class HTTPStatus(BaseCheck):
|
||||||
config = "status-is"
|
config = "status-is"
|
||||||
expected_cls = ExpectedIntValue
|
expected_cls = ExpectedIntValue
|
||||||
|
|
||||||
async def run(self) -> dict:
|
async def run(self, response: Response) -> dict:
|
||||||
# XXX Get the method from the task
|
|
||||||
task = self.task
|
|
||||||
response = await self.http_client.request(
|
|
||||||
method="get", url=task.url, timeout=60
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.response(
|
return self.response(
|
||||||
status=response.status_code == self.expected,
|
status=response.status_code == self.expected,
|
||||||
expected=self.expected,
|
expected=self.expected,
|
||||||
|
@ -42,13 +36,7 @@ class HTTPStatusIn(BaseCheck):
|
||||||
config = "status-in"
|
config = "status-in"
|
||||||
expected_cls = ExpectedStringValue
|
expected_cls = ExpectedStringValue
|
||||||
|
|
||||||
async def run(self) -> dict:
|
async def run(self, response: Response) -> dict:
|
||||||
# XXX Get the method from the task
|
|
||||||
task = self.task
|
|
||||||
response = await self.http_client.request(
|
|
||||||
method="get", url=task.url, timeout=60
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.response(
|
return self.response(
|
||||||
status=response.status_code in json.loads(self.expected),
|
status=response.status_code in json.loads(self.expected),
|
||||||
expected=self.expected,
|
expected=self.expected,
|
||||||
|
@ -62,11 +50,7 @@ class HTTPToHTTPS(BaseCheck):
|
||||||
config = "http-to-https"
|
config = "http-to-https"
|
||||||
expected_cls = ExpectedStringValue
|
expected_cls = ExpectedStringValue
|
||||||
|
|
||||||
async def run(self) -> dict:
|
async def run(self, response: Response) -> dict:
|
||||||
task = self.task
|
|
||||||
url = URL(task.url).copy_with(scheme="http")
|
|
||||||
response = await self.http_client.request(method="get", url=url, timeout=60)
|
|
||||||
|
|
||||||
expected_dict = json.loads(self.expected)
|
expected_dict = json.loads(self.expected)
|
||||||
expected = range(300, 400)
|
expected = range(300, 400)
|
||||||
if "range" in expected_dict:
|
if "range" in expected_dict:
|
||||||
|
@ -90,13 +74,7 @@ class HTTPHeadersContain(BaseCheck):
|
||||||
config = "headers-contain"
|
config = "headers-contain"
|
||||||
expected_cls = ExpectedStringValue
|
expected_cls = ExpectedStringValue
|
||||||
|
|
||||||
async def run(self) -> dict:
|
async def run(self, response: Response) -> dict:
|
||||||
# XXX Get the method from the task
|
|
||||||
task = self.task
|
|
||||||
response = await self.http_client.request(
|
|
||||||
method="get", url=task.url, timeout=60
|
|
||||||
)
|
|
||||||
|
|
||||||
status = True
|
status = True
|
||||||
for header in json.loads(self.expected):
|
for header in json.loads(self.expected):
|
||||||
if header not in response.headers:
|
if header not in response.headers:
|
||||||
|
@ -116,13 +94,7 @@ class HTTPHeadersHave(BaseCheck):
|
||||||
config = "headers-have"
|
config = "headers-have"
|
||||||
expected_cls = ExpectedStringValue
|
expected_cls = ExpectedStringValue
|
||||||
|
|
||||||
async def run(self) -> dict:
|
async def run(self, response: Response) -> dict:
|
||||||
# XXX Get the method from the task
|
|
||||||
task = self.task
|
|
||||||
response = await self.http_client.request(
|
|
||||||
method="get", url=task.url, timeout=60
|
|
||||||
)
|
|
||||||
|
|
||||||
status = True
|
status = True
|
||||||
for header, value in json.loads(self.expected).items():
|
for header, value in json.loads(self.expected).items():
|
||||||
if header not in response.headers:
|
if header not in response.headers:
|
||||||
|
@ -146,13 +118,7 @@ class HTTPHeadersLike(BaseCheck):
|
||||||
config = "headers-like"
|
config = "headers-like"
|
||||||
expected_cls = ExpectedStringValue
|
expected_cls = ExpectedStringValue
|
||||||
|
|
||||||
async def run(self) -> dict:
|
async def run(self, response: Response) -> dict:
|
||||||
# XXX Get the method from the task
|
|
||||||
task = self.task
|
|
||||||
response = await self.http_client.request(
|
|
||||||
method="get", url=task.url, timeout=60
|
|
||||||
)
|
|
||||||
|
|
||||||
status = True
|
status = True
|
||||||
for header, value in json.loads(self.expected).items():
|
for header, value in json.loads(self.expected).items():
|
||||||
if header not in response.headers:
|
if header not in response.headers:
|
||||||
|
@ -175,10 +141,7 @@ class HTTPBodyContains(BaseCheck):
|
||||||
config = "body-contains"
|
config = "body-contains"
|
||||||
expected_cls = ExpectedStringValue
|
expected_cls = ExpectedStringValue
|
||||||
|
|
||||||
async def run(self) -> dict:
|
async def run(self, response: Response) -> dict:
|
||||||
response = await self.http_client.request(
|
|
||||||
method="get", url=self.task.url, timeout=60
|
|
||||||
)
|
|
||||||
return self.response(status=self.expected in response.text)
|
return self.response(status=self.expected in response.text)
|
||||||
|
|
||||||
|
|
||||||
|
@ -188,10 +151,7 @@ class HTTPBodyLike(BaseCheck):
|
||||||
config = "body-like"
|
config = "body-like"
|
||||||
expected_cls = ExpectedStringValue
|
expected_cls = ExpectedStringValue
|
||||||
|
|
||||||
async def run(self) -> dict:
|
async def run(self, response: Response) -> dict:
|
||||||
response = await self.http_client.request(
|
|
||||||
method="get", url=self.task.url, timeout=60
|
|
||||||
)
|
|
||||||
if re.search(rf"{self.expected}", response.text):
|
if re.search(rf"{self.expected}", response.text):
|
||||||
return self.response(status=True)
|
return self.response(status=True)
|
||||||
|
|
||||||
|
@ -205,13 +165,7 @@ class HTTPJsonContains(BaseCheck):
|
||||||
config = "json-contains"
|
config = "json-contains"
|
||||||
expected_cls = ExpectedStringValue
|
expected_cls = ExpectedStringValue
|
||||||
|
|
||||||
async def run(self) -> dict:
|
async def run(self, response: Response) -> dict:
|
||||||
# XXX Get the method from the task
|
|
||||||
task = self.task
|
|
||||||
response = await self.http_client.request(
|
|
||||||
method="get", url=task.url, timeout=60
|
|
||||||
)
|
|
||||||
|
|
||||||
obj = response.json()
|
obj = response.json()
|
||||||
|
|
||||||
status = True
|
status = True
|
||||||
|
@ -235,13 +189,7 @@ class HTTPJsonHas(BaseCheck):
|
||||||
config = "json-has"
|
config = "json-has"
|
||||||
expected_cls = ExpectedStringValue
|
expected_cls = ExpectedStringValue
|
||||||
|
|
||||||
async def run(self) -> dict:
|
async def run(self, response: Response) -> dict:
|
||||||
# XXX Get the method from the task
|
|
||||||
task = self.task
|
|
||||||
response = await self.http_client.request(
|
|
||||||
method="get", url=task.url, timeout=60
|
|
||||||
)
|
|
||||||
|
|
||||||
obj = response.json()
|
obj = response.json()
|
||||||
|
|
||||||
status = True
|
status = True
|
||||||
|
@ -269,13 +217,7 @@ class HTTPJsonLike(BaseCheck):
|
||||||
config = "json-like"
|
config = "json-like"
|
||||||
expected_cls = ExpectedStringValue
|
expected_cls = ExpectedStringValue
|
||||||
|
|
||||||
async def run(self) -> dict:
|
async def run(self, response: Response) -> dict:
|
||||||
# XXX Get the method from the task
|
|
||||||
task = self.task
|
|
||||||
response = await self.http_client.request(
|
|
||||||
method="get", url=task.url, timeout=60
|
|
||||||
)
|
|
||||||
|
|
||||||
obj = response.json()
|
obj = response.json()
|
||||||
|
|
||||||
status = True
|
status = True
|
||||||
|
@ -302,13 +244,7 @@ class HTTPJsonIs(BaseCheck):
|
||||||
config = "json-is"
|
config = "json-is"
|
||||||
expected_cls = ExpectedStringValue
|
expected_cls = ExpectedStringValue
|
||||||
|
|
||||||
async def run(self) -> dict:
|
async def run(self, response: Response) -> dict:
|
||||||
# XXX Get the method from the task
|
|
||||||
task = self.task
|
|
||||||
response = await self.http_client.request(
|
|
||||||
method="get", url=task.url, timeout=60
|
|
||||||
)
|
|
||||||
|
|
||||||
obj = response.json()
|
obj = response.json()
|
||||||
|
|
||||||
status = response.json() == json.loads(self.expected)
|
status = response.json() == json.loads(self.expected)
|
||||||
|
@ -326,10 +262,8 @@ class SSLCertificateExpiration(BaseCheck):
|
||||||
config = "ssl-certificate-expiration"
|
config = "ssl-certificate-expiration"
|
||||||
expected_cls = ExpectedStringValue
|
expected_cls = ExpectedStringValue
|
||||||
|
|
||||||
async def run(self):
|
async def run(self, response: Response) -> dict:
|
||||||
"""Returns the number of days in which the certificate will expire."""
|
"""Returns the number of days in which the certificate will expire."""
|
||||||
response = await self.http_client.get(self.task.url, timeout=60)
|
|
||||||
|
|
||||||
network_stream = response.extensions["network_stream"]
|
network_stream = response.extensions["network_stream"]
|
||||||
ssl_obj = network_stream.get_extra_info("ssl_object")
|
ssl_obj = network_stream.get_extra_info("ssl_object")
|
||||||
cert = ssl_obj.getpeercert()
|
cert = ssl_obj.getpeercert()
|
||||||
|
|
|
@ -14,9 +14,19 @@ general:
|
||||||
# Can be "production", "dev", "test".
|
# Can be "production", "dev", "test".
|
||||||
# If not present, default value is "production"
|
# If not present, default value is "production"
|
||||||
env: "production"
|
env: "production"
|
||||||
# to get a good string for cookie_secret, run:
|
# To get a good string for cookie_secret, run:
|
||||||
# openssl rand -hex 32
|
# openssl rand -hex 32
|
||||||
cookie_secret: "foo_bar_baz"
|
cookie_secret: "foo_bar_baz"
|
||||||
|
|
||||||
|
# Session duration
|
||||||
|
# Use m for minutes, h for hours, d for days
|
||||||
|
# w for weeks, mo for months, y for years
|
||||||
|
# If not present, default value is "7d"
|
||||||
|
session_duration: "7d"
|
||||||
|
# Session opened with "Remember me" checked
|
||||||
|
# If not present, the "Remember me" feature is not available
|
||||||
|
# remember_me_duration: "1mo"
|
||||||
|
|
||||||
# Default delay for checks.
|
# Default delay for checks.
|
||||||
# Can be superseeded in domain configuration.
|
# Can be superseeded in domain configuration.
|
||||||
# For ex., to run checks every minute:
|
# For ex., to run checks every minute:
|
||||||
|
@ -93,6 +103,11 @@ websites:
|
||||||
- domain: "https://mypads.example.org"
|
- domain: "https://mypads.example.org"
|
||||||
paths:
|
paths:
|
||||||
- path: "/mypads/"
|
- path: "/mypads/"
|
||||||
|
# Specify the method of the HTTP request
|
||||||
|
# Valid values are "GET", "HEAD", "POST", "OPTIONS",
|
||||||
|
# "CONNECT", "TRACE", "PUT", "PATCH" and "DELETE"
|
||||||
|
# default is "GET" if omitted
|
||||||
|
method: "GET"
|
||||||
checks:
|
checks:
|
||||||
# Check that the returned HTTP status is 200
|
# Check that the returned HTTP status is 200
|
||||||
- status-is: 200
|
- status-is: 200
|
||||||
|
|
|
@ -22,7 +22,7 @@ from pydantic.networks import UrlConstraints
|
||||||
from pydantic_core import Url
|
from pydantic_core import Url
|
||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
from argos.schemas.utils import string_to_duration
|
from argos.schemas.utils import string_to_duration, Method
|
||||||
|
|
||||||
Severity = Literal["warning", "error", "critical", "unknown"]
|
Severity = Literal["warning", "error", "critical", "unknown"]
|
||||||
Environment = Literal["dev", "test", "production"]
|
Environment = Literal["dev", "test", "production"]
|
||||||
|
@ -104,6 +104,7 @@ def parse_checks(value):
|
||||||
|
|
||||||
class WebsitePath(BaseModel):
|
class WebsitePath(BaseModel):
|
||||||
path: str
|
path: str
|
||||||
|
method: Method = "GET"
|
||||||
checks: List[
|
checks: List[
|
||||||
Annotated[
|
Annotated[
|
||||||
Tuple[str, str],
|
Tuple[str, str],
|
||||||
|
@ -174,16 +175,31 @@ class DbSettings(BaseModel):
|
||||||
class General(BaseModel):
|
class General(BaseModel):
|
||||||
"""Frequency for the checks and alerts"""
|
"""Frequency for the checks and alerts"""
|
||||||
|
|
||||||
cookie_secret: str
|
|
||||||
frequency: int
|
|
||||||
db: DbSettings
|
db: DbSettings
|
||||||
env: Environment = "production"
|
env: Environment = "production"
|
||||||
|
cookie_secret: str
|
||||||
|
session_duration: int = 10080 # 7 days
|
||||||
|
remember_me_duration: Optional[int] = None
|
||||||
|
frequency: int
|
||||||
root_path: str = ""
|
root_path: str = ""
|
||||||
alerts: Alert
|
alerts: Alert
|
||||||
mail: Optional[Mail] = None
|
mail: Optional[Mail] = None
|
||||||
gotify: Optional[List[GotifyUrl]] = None
|
gotify: Optional[List[GotifyUrl]] = None
|
||||||
apprise: Optional[Dict[str, List[str]]] = None
|
apprise: Optional[Dict[str, List[str]]] = None
|
||||||
|
|
||||||
|
@field_validator("session_duration", mode="before")
|
||||||
|
def parse_session_duration(cls, value):
|
||||||
|
"""Convert the configured session duration to minutes"""
|
||||||
|
return string_to_duration(value, "minutes")
|
||||||
|
|
||||||
|
@field_validator("remember_me_duration", mode="before")
|
||||||
|
def parse_remember_me_duration(cls, value):
|
||||||
|
"""Convert the configured session duration with remember me feature to minutes"""
|
||||||
|
if value:
|
||||||
|
return string_to_duration(value, "minutes")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
@field_validator("frequency", mode="before")
|
@field_validator("frequency", mode="before")
|
||||||
def parse_frequency(cls, value):
|
def parse_frequency(cls, value):
|
||||||
"""Convert the configured frequency to minutes"""
|
"""Convert the configured frequency to minutes"""
|
||||||
|
|
|
@ -8,6 +8,8 @@ from typing import Literal
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
from argos.schemas.utils import Method
|
||||||
|
|
||||||
# XXX Refactor using SQLModel to avoid duplication of model data
|
# XXX Refactor using SQLModel to avoid duplication of model data
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,6 +20,7 @@ class Task(BaseModel):
|
||||||
url: str
|
url: str
|
||||||
domain: str
|
domain: str
|
||||||
check: str
|
check: str
|
||||||
|
method: Method
|
||||||
expected: str
|
expected: str
|
||||||
selected_at: datetime | None
|
selected_at: datetime | None
|
||||||
selected_by: str | None
|
selected_by: str | None
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
|
|
||||||
|
Method = Literal[
|
||||||
|
"GET", "HEAD", "POST", "OPTIONS", "CONNECT", "TRACE", "PUT", "PATCH", "DELETE"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def string_to_duration(
|
def string_to_duration(
|
||||||
value: str, target: Literal["days", "hours", "minutes"]
|
value: str, target: Literal["days", "hours", "minutes"]
|
||||||
) -> int | float:
|
) -> int | float:
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
"""Specify check method
|
||||||
|
|
||||||
|
Revision ID: dcf73fa19fce
|
||||||
|
Revises: c780864dc407
|
||||||
|
Create Date: 2024-11-26 14:40:27.510587
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "dcf73fa19fce"
|
||||||
|
down_revision: Union[str, None] = "c780864dc407"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
with op.batch_alter_table("tasks", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(
|
||||||
|
sa.Column(
|
||||||
|
"method",
|
||||||
|
sa.Enum(
|
||||||
|
"GET",
|
||||||
|
"HEAD",
|
||||||
|
"POST",
|
||||||
|
"OPTIONS",
|
||||||
|
"CONNECT",
|
||||||
|
"TRACE",
|
||||||
|
"PUT",
|
||||||
|
"PATCH",
|
||||||
|
"DELETE",
|
||||||
|
name="method",
|
||||||
|
),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
with op.batch_alter_table("tasks", schema=None) as batch_op:
|
||||||
|
batch_op.drop_column("method")
|
|
@ -12,6 +12,7 @@ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from argos.checks import BaseCheck, get_registered_check
|
from argos.checks import BaseCheck, get_registered_check
|
||||||
from argos.schemas import WebsiteCheck
|
from argos.schemas import WebsiteCheck
|
||||||
|
from argos.schemas.utils import Method
|
||||||
|
|
||||||
|
|
||||||
class Base(DeclarativeBase):
|
class Base(DeclarativeBase):
|
||||||
|
@ -35,6 +36,21 @@ class Task(Base):
|
||||||
check: Mapped[str] = mapped_column()
|
check: Mapped[str] = mapped_column()
|
||||||
expected: Mapped[str] = mapped_column()
|
expected: Mapped[str] = mapped_column()
|
||||||
frequency: Mapped[int] = mapped_column()
|
frequency: Mapped[int] = mapped_column()
|
||||||
|
method: Mapped[Method] = mapped_column(
|
||||||
|
Enum(
|
||||||
|
"GET",
|
||||||
|
"HEAD",
|
||||||
|
"POST",
|
||||||
|
"OPTIONS",
|
||||||
|
"CONNECT",
|
||||||
|
"TRACE",
|
||||||
|
"PUT",
|
||||||
|
"PATCH",
|
||||||
|
"DELETE",
|
||||||
|
name="method",
|
||||||
|
),
|
||||||
|
insert_default="GET",
|
||||||
|
)
|
||||||
|
|
||||||
# Orchestration-related
|
# Orchestration-related
|
||||||
selected_by: Mapped[str] = mapped_column(nullable=True)
|
selected_by: Mapped[str] = mapped_column(nullable=True)
|
||||||
|
|
|
@ -146,6 +146,7 @@ async def update_from_config(db: Session, config: schemas.Config):
|
||||||
db.query(Task)
|
db.query(Task)
|
||||||
.filter(
|
.filter(
|
||||||
Task.url == url,
|
Task.url == url,
|
||||||
|
Task.method == p.method,
|
||||||
Task.check == check_key,
|
Task.check == check_key,
|
||||||
Task.expected == expected,
|
Task.expected == expected,
|
||||||
)
|
)
|
||||||
|
@ -159,8 +160,10 @@ async def update_from_config(db: Session, config: schemas.Config):
|
||||||
existing_task.frequency = frequency
|
existing_task.frequency = frequency
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Skipping db task creation for url=%s, "
|
"Skipping db task creation for url=%s, "
|
||||||
"check_key=%s, expected=%s, frequency=%s.",
|
"method=%s, check_key=%s, expected=%s, "
|
||||||
|
"frequency=%s.",
|
||||||
url,
|
url,
|
||||||
|
p.method,
|
||||||
check_key,
|
check_key,
|
||||||
expected,
|
expected,
|
||||||
frequency,
|
frequency,
|
||||||
|
@ -173,6 +176,7 @@ async def update_from_config(db: Session, config: schemas.Config):
|
||||||
task = Task(
|
task = Task(
|
||||||
domain=domain,
|
domain=domain,
|
||||||
url=url,
|
url=url,
|
||||||
|
method=p.method,
|
||||||
check=check_key,
|
check=check_key,
|
||||||
expected=expected,
|
expected=expected,
|
||||||
frequency=frequency,
|
frequency=frequency,
|
||||||
|
|
|
@ -28,7 +28,11 @@ SEVERITY_LEVELS = {"ok": 1, "warning": 2, "critical": 3, "unknown": 4}
|
||||||
|
|
||||||
|
|
||||||
@route.get("/login")
|
@route.get("/login")
|
||||||
async def login_view(request: Request, msg: str | None = None):
|
async def login_view(
|
||||||
|
request: Request,
|
||||||
|
msg: str | None = None,
|
||||||
|
config: Config = Depends(get_config),
|
||||||
|
):
|
||||||
token = request.cookies.get("access-token")
|
token = request.cookies.get("access-token")
|
||||||
if token is not None and token != "":
|
if token is not None and token != "":
|
||||||
manager = request.app.state.manager
|
manager = request.app.state.manager
|
||||||
|
@ -44,7 +48,14 @@ async def login_view(request: Request, msg: str | None = None):
|
||||||
else:
|
else:
|
||||||
msg = None
|
msg = None
|
||||||
|
|
||||||
return templates.TemplateResponse("login.html", {"request": request, "msg": msg})
|
return templates.TemplateResponse(
|
||||||
|
"login.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"msg": msg,
|
||||||
|
"remember": config.general.remember_me_duration,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@route.post("/login")
|
@route.post("/login")
|
||||||
|
@ -52,6 +63,8 @@ async def post_login(
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
data: OAuth2PasswordRequestForm = Depends(),
|
data: OAuth2PasswordRequestForm = Depends(),
|
||||||
|
rememberme: Annotated[str | None, Form()] = None,
|
||||||
|
config: Config = Depends(get_config),
|
||||||
):
|
):
|
||||||
username = data.username
|
username = data.username
|
||||||
user = await queries.get_user(db, username)
|
user = await queries.get_user(db, username)
|
||||||
|
@ -70,14 +83,22 @@ async def post_login(
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
manager = request.app.state.manager
|
manager = request.app.state.manager
|
||||||
token = manager.create_access_token(
|
session_duration = config.general.session_duration
|
||||||
data={"sub": username}, expires=timedelta(days=7)
|
if config.general.remember_me_duration is not None and rememberme == "on":
|
||||||
)
|
session_duration = config.general.remember_me_duration
|
||||||
|
delta = timedelta(minutes=session_duration)
|
||||||
|
token = manager.create_access_token(data={"sub": username}, expires=delta)
|
||||||
response = RedirectResponse(
|
response = RedirectResponse(
|
||||||
request.url_for("get_severity_counts_view"),
|
request.url_for("get_severity_counts_view"),
|
||||||
status_code=status.HTTP_303_SEE_OTHER,
|
status_code=status.HTTP_303_SEE_OTHER,
|
||||||
)
|
)
|
||||||
manager.set_cookie(response, token)
|
response.set_cookie(
|
||||||
|
key=manager.cookie_name,
|
||||||
|
value=token,
|
||||||
|
httponly=True,
|
||||||
|
samesite="strict",
|
||||||
|
expires=int(delta.total_seconds()),
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,15 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{# djlint:off H021 #}
|
{# djlint:off H021 #}
|
||||||
<ul id="status-selector" style="display: none;">{# djlint:on #}
|
<ul id="js-only" style="display: none; ">{# djlint:on #}
|
||||||
|
<li>
|
||||||
|
<input id="domain-search"
|
||||||
|
type="search"
|
||||||
|
spellcheck="false"
|
||||||
|
placeholder="Filter domains list"
|
||||||
|
aria-label="Filter domains list"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label for="select-status">Show domains with status:</label>
|
<label for="select-status">Show domains with status:</label>
|
||||||
<select id="select-status">
|
<select id="select-status">
|
||||||
|
@ -38,7 +46,8 @@
|
||||||
|
|
||||||
<tbody id="domains-body">
|
<tbody id="domains-body">
|
||||||
{% for (domain, status) in domains %}
|
{% for (domain, status) in domains %}
|
||||||
<tr data-status="{{ status }}">
|
<tr data-status="{{ status }}"
|
||||||
|
data-domain="{{ domain }}">
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ url_for('get_domain_tasks_view', domain=domain) }}">
|
<a href="{{ url_for('get_domain_tasks_view', domain=domain) }}">
|
||||||
{{ domain }}
|
{{ domain }}
|
||||||
|
@ -62,12 +71,46 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
document.getElementById('select-status').addEventListener('change', (e) => {
|
function filterDomains(e) {
|
||||||
if (e.currentTarget.value === 'all') {
|
let status = document.getElementById('select-status');
|
||||||
|
let filter = document.getElementById('domain-search').value;
|
||||||
|
console.log(filter)
|
||||||
|
if (status.value === 'all') {
|
||||||
document.querySelectorAll('[data-status]').forEach((item) => {
|
document.querySelectorAll('[data-status]').forEach((item) => {
|
||||||
|
if (filter && item.dataset.domain.indexOf(filter) == -1) {
|
||||||
|
item.style.display = 'none';
|
||||||
|
} else {
|
||||||
item.style.display = null;
|
item.style.display = null;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else if (e.currentTarget.value === 'not-ok') {
|
} else if (status.value === 'not-ok') {
|
||||||
|
document.querySelectorAll('[data-status]').forEach((item) => {
|
||||||
|
if (item.dataset.status !== 'ok') {
|
||||||
|
if (filter && item.dataset.domain.indexOf(filter) == -1) {
|
||||||
|
item.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
item.style.display = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
document.querySelectorAll('[data-status]').forEach((item) => {
|
||||||
|
if (item.dataset.status === status.value) {
|
||||||
|
if (filter && item.dataset.domain.indexOf(filter) == -1) {
|
||||||
|
item.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
item.style.display = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('select-status').addEventListener('change', filterDomains);
|
||||||
|
document.getElementById('domain-search').addEventListener('input', filterDomains);
|
||||||
document.querySelectorAll('[data-status]').forEach((item) => {
|
document.querySelectorAll('[data-status]').forEach((item) => {
|
||||||
if (item.dataset.status !== 'ok') {
|
if (item.dataset.status !== 'ok') {
|
||||||
item.style.display = null;
|
item.style.display = null;
|
||||||
|
@ -75,23 +118,6 @@
|
||||||
item.style.display = 'none';
|
item.style.display = 'none';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
document.getElementById('js-only').style.display = null;
|
||||||
document.querySelectorAll('[data-status]').forEach((item) => {
|
|
||||||
if (item.dataset.status === e.currentTarget.value) {
|
|
||||||
item.style.display = null;
|
|
||||||
} else {
|
|
||||||
item.style.display = 'none';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.querySelectorAll('[data-status]').forEach((item) => {
|
|
||||||
if (item.dataset.status !== 'ok') {
|
|
||||||
item.style.display = null;
|
|
||||||
} else {
|
|
||||||
item.style.display = 'none';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
document.getElementById('status-selector').style.display = null;
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
|
@ -16,6 +16,14 @@
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
form="login">
|
form="login">
|
||||||
|
{% if remember is not none %}
|
||||||
|
<label>
|
||||||
|
<input type="checkbox"
|
||||||
|
name="rememberme"
|
||||||
|
form="login">
|
||||||
|
Remember me
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
<form id="login"
|
<form id="login"
|
||||||
method="post"
|
method="post"
|
||||||
action="{{ url_for('post_login') }}">
|
action="{{ url_for('post_login') }}">
|
||||||
|
|
|
@ -10,7 +10,8 @@ First, do your changes in the code, change the model, add new tables, etc. Once
|
||||||
you're done, you can create a new migration.
|
you're done, you can create a new migration.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
venv/bin/alembic -c argos/server/migrations/alembic.ini revision --autogenerate -m "migration reason"
|
venv/bin/alembic -c argos/server/migrations/alembic.ini revision \
|
||||||
|
--autogenerate -m "migration reason"
|
||||||
```
|
```
|
||||||
|
|
||||||
Edit the created file to remove comments and adapt it to make sure the migration is complete (Alembic is not powerful enough to cover all the corner cases).
|
Edit the created file to remove comments and adapt it to make sure the migration is complete (Alembic is not powerful enough to cover all the corner cases).
|
||||||
|
|
|
@ -35,6 +35,7 @@ def ssl_task(now):
|
||||||
id=1,
|
id=1,
|
||||||
url="https://example.org",
|
url="https://example.org",
|
||||||
domain="https://example.org",
|
domain="https://example.org",
|
||||||
|
method="GET",
|
||||||
check="ssl-certificate-expiration",
|
check="ssl-certificate-expiration",
|
||||||
expected="on-check",
|
expected="on-check",
|
||||||
selected_at=now,
|
selected_at=now,
|
||||||
|
@ -51,6 +52,9 @@ async def test_ssl_check_accepts_statuts(
|
||||||
return_value=httpx.Response(http_status, extensions=httpx_extensions_ssl),
|
return_value=httpx.Response(http_status, extensions=httpx_extensions_ssl),
|
||||||
)
|
)
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
check = SSLCertificateExpiration(client, ssl_task)
|
check = SSLCertificateExpiration(ssl_task)
|
||||||
check_response = await check.run()
|
response = await client.request(
|
||||||
|
method=ssl_task.method, url=ssl_task.url, timeout=60
|
||||||
|
)
|
||||||
|
check_response = await check.run(response)
|
||||||
assert check_response.status == "on-check"
|
assert check_response.status == "on-check"
|
||||||
|
|
Loading…
Reference in a new issue