342 lines
11 KiB
Elixir
342 lines
11 KiB
Elixir
defmodule Towerops.Admin.AuditLoggerTest do
|
|
use Towerops.DataCase, async: true
|
|
|
|
import Towerops.AccountsFixtures
|
|
|
|
alias Towerops.Admin.AuditLog
|
|
alias Towerops.Admin.AuditLogger
|
|
|
|
setup do
|
|
actor = user_fixture()
|
|
target = user_fixture()
|
|
|
|
%{actor: actor, target: target}
|
|
end
|
|
|
|
describe "log_event/3" do
|
|
test "creates an audit log with a conn", %{actor: actor, target: target} do
|
|
conn =
|
|
:get
|
|
|> Plug.Test.conn("/admin/users")
|
|
|> Plug.Conn.put_req_header("user-agent", "TestBrowser/1.0")
|
|
|> Plug.Conn.put_req_header("x-forwarded-for", "203.0.113.50")
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_event("user_data_viewed", conn,
|
|
actor_id: actor.id,
|
|
target_user_id: target.id,
|
|
metadata: %{reason: "support ticket"}
|
|
)
|
|
|
|
assert log.action == "user_data_viewed"
|
|
assert log.superuser_id == actor.id
|
|
assert log.target_user_id == target.id
|
|
assert log.ip_address.address == "203.0.113.50"
|
|
assert log.user_agent == "TestBrowser/1.0"
|
|
assert log.request_path == "/admin/users"
|
|
assert log.metadata.reason == "support ticket"
|
|
end
|
|
|
|
test "creates an audit log without a conn (nil)", %{actor: actor} do
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_event("user_data_exported", nil,
|
|
actor_id: actor.id,
|
|
target_user_id: actor.id,
|
|
metadata: %{export_format: "json"}
|
|
)
|
|
|
|
assert log.action == "user_data_exported"
|
|
assert log.superuser_id == actor.id
|
|
assert log.ip_address == nil
|
|
assert log.user_agent == nil
|
|
assert log.request_path == nil
|
|
end
|
|
|
|
test "creates an audit log with no opts", %{actor: _actor} do
|
|
conn = Plug.Test.conn(:get, "/test")
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_event("user_data_exported", conn)
|
|
|
|
assert log.action == "user_data_exported"
|
|
assert log.superuser_id == nil
|
|
assert log.target_user_id == nil
|
|
assert log.request_path == "/test"
|
|
end
|
|
|
|
test "returns error for invalid action" do
|
|
assert {:error, changeset} = AuditLogger.log_event("bogus_action")
|
|
assert %{action: ["is invalid"]} = errors_on(changeset)
|
|
end
|
|
|
|
test "stores data_accessed when provided", %{actor: actor, target: target} do
|
|
data = %{fields: ["email", "phone"]}
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_event("user_data_viewed", nil,
|
|
actor_id: actor.id,
|
|
target_user_id: target.id,
|
|
data_accessed: data
|
|
)
|
|
|
|
assert log.data_accessed.fields == ["email", "phone"]
|
|
end
|
|
end
|
|
|
|
describe "log_user_data_viewed/4" do
|
|
test "logs user data viewed event with conn metadata", %{actor: actor, target: target} do
|
|
conn =
|
|
:get
|
|
|> Plug.Test.conn("/admin/users/#{target.id}")
|
|
|> Plug.Conn.put_req_header("user-agent", "Mozilla/5.0")
|
|
|> Plug.Conn.put_req_header("x-forwarded-for", "10.0.0.1")
|
|
|
|
data_accessed = %{fields: ["email", "name"]}
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_user_data_viewed(conn, actor.id, target.id, data_accessed)
|
|
|
|
assert log.action == "user_data_viewed"
|
|
assert log.superuser_id == actor.id
|
|
assert log.target_user_id == target.id
|
|
assert log.data_accessed.fields == ["email", "name"]
|
|
assert log.ip_address.address == "10.0.0.1"
|
|
assert log.user_agent == "Mozilla/5.0"
|
|
end
|
|
end
|
|
|
|
describe "log_user_data_exported/2" do
|
|
test "logs user data export event", %{actor: actor} do
|
|
conn = Plug.Test.conn(:post, "/settings/export")
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_user_data_exported(conn, actor.id)
|
|
|
|
assert log.action == "user_data_exported"
|
|
assert log.superuser_id == actor.id
|
|
assert log.target_user_id == actor.id
|
|
assert log.metadata.export_format == "json"
|
|
end
|
|
end
|
|
|
|
describe "log_user_profile_updated/3" do
|
|
test "logs profile update with changed fields", %{actor: actor} do
|
|
conn = Plug.Test.conn(:put, "/settings/profile")
|
|
fields_changed = ["email", "name"]
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_user_profile_updated(conn, actor.id, fields_changed)
|
|
|
|
assert log.action == "user_profile_updated"
|
|
assert log.superuser_id == actor.id
|
|
assert log.target_user_id == actor.id
|
|
assert log.data_accessed.fields_changed == ["email", "name"]
|
|
end
|
|
end
|
|
|
|
describe "log_device_created/4" do
|
|
test "logs device creation with metadata", %{actor: actor} do
|
|
conn = Plug.Test.conn(:post, "/devices")
|
|
device_id = Ecto.UUID.generate()
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_device_created(conn, actor.id, device_id, "Core Router")
|
|
|
|
assert log.action == "device_created"
|
|
assert log.superuser_id == actor.id
|
|
assert log.metadata.device_id == device_id
|
|
assert log.metadata.device_name == "Core Router"
|
|
end
|
|
end
|
|
|
|
describe "log_device_updated/4" do
|
|
test "logs device update with changed fields", %{actor: actor} do
|
|
conn = Plug.Test.conn(:put, "/devices/1")
|
|
device_id = Ecto.UUID.generate()
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_device_updated(conn, actor.id, device_id, ["hostname", "ip"])
|
|
|
|
assert log.action == "device_updated"
|
|
assert log.superuser_id == actor.id
|
|
assert log.metadata.device_id == device_id
|
|
assert log.data_accessed.fields_changed == ["hostname", "ip"]
|
|
end
|
|
end
|
|
|
|
describe "log_device_deleted/4" do
|
|
test "logs device deletion with metadata", %{actor: actor} do
|
|
conn = Plug.Test.conn(:delete, "/devices/1")
|
|
device_id = Ecto.UUID.generate()
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_device_deleted(conn, actor.id, device_id, "Old Switch")
|
|
|
|
assert log.action == "device_deleted"
|
|
assert log.superuser_id == actor.id
|
|
assert log.metadata.device_id == device_id
|
|
assert log.metadata.device_name == "Old Switch"
|
|
end
|
|
end
|
|
|
|
describe "log_org_data_accessed/4" do
|
|
test "logs organization data access", %{actor: actor} do
|
|
conn = Plug.Test.conn(:get, "/orgs/settings")
|
|
org_id = Ecto.UUID.generate()
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_org_data_accessed(conn, actor.id, org_id, "viewed_members")
|
|
|
|
assert log.action == "org_data_accessed"
|
|
assert log.superuser_id == actor.id
|
|
assert log.metadata.organization_id == org_id
|
|
assert log.metadata.action == "viewed_members"
|
|
end
|
|
end
|
|
|
|
describe "log_monitoring_data_queried/4" do
|
|
test "logs monitoring query event", %{actor: actor} do
|
|
conn = Plug.Test.conn(:get, "/monitoring")
|
|
device_id = Ecto.UUID.generate()
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_monitoring_data_queried(conn, actor.id, device_id, "24h")
|
|
|
|
assert log.action == "monitoring_data_queried"
|
|
assert log.superuser_id == actor.id
|
|
assert log.metadata.device_id == device_id
|
|
assert log.metadata.time_range == "24h"
|
|
end
|
|
end
|
|
|
|
describe "log_failed_access_attempt/4" do
|
|
test "logs failed access with resource and reason", %{actor: actor} do
|
|
conn =
|
|
:get
|
|
|> Plug.Test.conn("/admin/secret")
|
|
|> Plug.Conn.put_req_header("x-forwarded-for", "192.168.1.100")
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_failed_access_attempt(
|
|
conn,
|
|
actor.id,
|
|
"/admin/secret",
|
|
"insufficient_permissions"
|
|
)
|
|
|
|
assert log.action == "failed_access_attempt"
|
|
assert log.superuser_id == actor.id
|
|
assert log.metadata.resource == "/admin/secret"
|
|
assert log.metadata.reason == "insufficient_permissions"
|
|
assert log.ip_address.address == "192.168.1.100"
|
|
end
|
|
end
|
|
|
|
describe "log_privilege_escalation/5" do
|
|
test "logs role change with context", %{actor: actor} do
|
|
conn = Plug.Test.conn(:post, "/admin/roles")
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_privilege_escalation(
|
|
conn,
|
|
actor.id,
|
|
"member",
|
|
"admin",
|
|
"org_promotion"
|
|
)
|
|
|
|
assert log.action == "privilege_escalation"
|
|
assert log.superuser_id == actor.id
|
|
assert log.metadata.from_role == "member"
|
|
assert log.metadata.to_role == "admin"
|
|
assert log.metadata.context == "org_promotion"
|
|
end
|
|
end
|
|
|
|
describe "IP address extraction" do
|
|
test "extracts IP from x-forwarded-for header", %{actor: actor} do
|
|
conn =
|
|
:get
|
|
|> Plug.Test.conn("/test")
|
|
|> Plug.Conn.put_req_header("x-forwarded-for", "198.51.100.42")
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_event("user_data_exported", conn,
|
|
actor_id: actor.id,
|
|
target_user_id: actor.id
|
|
)
|
|
|
|
assert log.ip_address.address == "198.51.100.42"
|
|
end
|
|
|
|
test "extracts first IP from comma-separated x-forwarded-for", %{actor: actor} do
|
|
conn =
|
|
:get
|
|
|> Plug.Test.conn("/test")
|
|
|> Plug.Conn.put_req_header("x-forwarded-for", "198.51.100.42, 10.0.0.1, 172.16.0.1")
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_event("user_data_exported", conn,
|
|
actor_id: actor.id,
|
|
target_user_id: actor.id
|
|
)
|
|
|
|
assert log.ip_address.address == "198.51.100.42"
|
|
end
|
|
|
|
test "falls back to remote_ip when x-forwarded-for is absent", %{actor: actor} do
|
|
conn = %{Plug.Test.conn(:get, "/test") | remote_ip: {10, 20, 30, 40}}
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_event("user_data_exported", conn,
|
|
actor_id: actor.id,
|
|
target_user_id: actor.id
|
|
)
|
|
|
|
assert log.ip_address.address == "10.20.30.40"
|
|
end
|
|
end
|
|
|
|
describe "user agent extraction" do
|
|
test "extracts user-agent header from conn", %{actor: actor} do
|
|
conn =
|
|
:get
|
|
|> Plug.Test.conn("/test")
|
|
|> Plug.Conn.put_req_header("user-agent", "CustomAgent/2.0")
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_event("user_data_exported", conn,
|
|
actor_id: actor.id,
|
|
target_user_id: actor.id
|
|
)
|
|
|
|
assert log.user_agent == "CustomAgent/2.0"
|
|
end
|
|
|
|
test "returns nil user_agent when header is absent", %{actor: actor} do
|
|
conn = Plug.Test.conn(:get, "/test")
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_event("user_data_exported", conn,
|
|
actor_id: actor.id,
|
|
target_user_id: actor.id
|
|
)
|
|
|
|
assert log.user_agent == nil
|
|
end
|
|
end
|
|
|
|
describe "request path extraction" do
|
|
test "extracts request_path from conn", %{actor: actor} do
|
|
conn = Plug.Test.conn(:get, "/admin/dashboard")
|
|
|
|
assert {:ok, %AuditLog{} = log} =
|
|
AuditLogger.log_event("user_data_exported", conn,
|
|
actor_id: actor.id,
|
|
target_user_id: actor.id
|
|
)
|
|
|
|
assert log.request_path == "/admin/dashboard"
|
|
end
|
|
end
|
|
end
|