/*_############################################################################
  _##
  _##  SNMP4J-Agent 3 - DefaultAgentAuditPolicyTest.java
  _##
  _##  Copyright (C) 2005-2026  Frank Fock (SNMP4J.org)
  _##
  _##  Licensed under the Apache License, Version 2.0 (the "License");
  _##  you may not use this file except in compliance with the License.
  _##  You may obtain a copy of the License at
  _##
  _##      http://www.apache.org/licenses/LICENSE-2.0
  _##
  _##  Unless required by applicable law or agreed to in writing, software
  _##  distributed under the License is distributed on an "AS IS" BASIS,
  _##  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  _##  See the License for the specific language governing permissions and
  _##  limitations under the License.
  _##
  _##########################################################################*/
package org.snmp4j.agent.audit;

import org.junit.Test;
import org.snmp4j.security.SecurityLevel;
import org.snmp4j.smi.UdpAddress;
import org.snmp4j.transport.TransportAuditEvent;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class DefaultAgentAuditPolicyTest {

    private static AgentPolicyProfile localReadOnlyWorkingDirProfile() {
        return new AgentPolicyProfile(
                AgentPolicyProfile.ManagedObjectAccess.READ,
                AgentPolicyProfile.NetworkAccess.LOOPBACK,
                AgentPolicyProfile.FileAccess.WORKING_DIRECTORY_READWRITE,
                SecurityLevel.authPriv);
    }

    private static AgentPolicyProfile workingDirectoryReadOnlyProfile() {
        return new AgentPolicyProfile(
                AgentPolicyProfile.ManagedObjectAccess.READ,
                AgentPolicyProfile.NetworkAccess.LOOPBACK,
                AgentPolicyProfile.FileAccess.WORKING_DIRECTORY_READONLY,
                SecurityLevel.authPriv);
    }

    private static AgentPolicyProfile networkIPv4ByCIDRReadOnlyWorkingDirProfile() {
        return new AgentPolicyProfile(
                AgentPolicyProfile.ManagedObjectAccess.READ,
                AgentPolicyProfile.NetworkAccess.REMOTE_NETWORK_LIMITED,
                AgentPolicyProfile.FileAccess.WORKING_DIRECTORY_READWRITE,
                SecurityLevel.authPriv,
                null, null, new String[]{"10.90.0.0/24/45000"},
                null);
    }

    private static AgentPolicyProfile networkIPv6ByCIDRReadOnlyWorkingDirProfile() {
        return new AgentPolicyProfile(
                AgentPolicyProfile.ManagedObjectAccess.READ,
                AgentPolicyProfile.NetworkAccess.REMOTE_NETWORK_LIMITED,
                AgentPolicyProfile.FileAccess.WORKING_DIRECTORY_READWRITE,
                SecurityLevel.authPriv,
                null, null, new String[]{"2001:db8::/32/45000"},
                null);
    }

    private static TransportAuditEvent receive(String peer) {
        return new TransportAuditEvent(new Object(), TransportAuditEvent.TransportOperation.RECEIVE,
                new UdpAddress(peer), new UdpAddress("0.0.0.0/161"), new byte[]{1, 2, 3});
    }

    @Test
    public void offPermitsEverythingAndDoesNotLog() {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DefaultAgentAuditPolicy policy =
                new DefaultAgentAuditPolicy(localReadOnlyWorkingDirProfile(), AgentAuditLevel.OFF, out);
        // remote peer would violate LOCAL_NETWORK, but OFF permits and does not audit
        assertTrue(policy.transportAudit(receive("8.8.8.8/12345")));
        assertTrue(policy.fileAudit(new FileAuditEvent(this,
                FileAuditEvent.FileOperation.DELETE, new File("/etc/passwd"))));
        assertEquals("", out.toString());
    }

    @Test
    public void restrictDeniesRemoteTransportAndLogsWarning() {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DefaultAgentAuditPolicy policy =
                new DefaultAgentAuditPolicy(localReadOnlyWorkingDirProfile(), AgentAuditLevel.RESTRICT, out);
        // local/loopback peer is permitted
        assertTrue(policy.transportAudit(receive("127.0.0.1/45000")));
        // remote peer is denied under LOCAL_NETWORK + RESTRICT
        assertFalse(policy.transportAudit(receive("8.8.8.8/12345")));
        String log = new String(out.toByteArray(), StandardCharsets.UTF_8);
        assertTrue(log.contains("permitted"));
        assertTrue(log.contains("DENIED"));
        assertTrue(log.contains("TRANSPORT"));
    }

    @Test
    public void restrictDeniesRemoteTransportByCIDRAndLogsWarning() {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DefaultAgentAuditPolicy policy =
                new DefaultAgentAuditPolicy(networkIPv4ByCIDRReadOnlyWorkingDirProfile(), AgentAuditLevel.RESTRICT, out);
        // local/loopback peer is permitted
        assertTrue(policy.transportAudit(receive("10.90.0.1/45000")));
        // remote peer is denied under LOCAL_NETWORK + RESTRICT
        assertFalse(policy.transportAudit(receive("8.8.8.8/12345")));
        String log = new String(out.toByteArray(), StandardCharsets.UTF_8);
        assertTrue(log.contains("permitted"));
        assertTrue(log.contains("DENIED"));
        assertTrue(log.contains("TRANSPORT"));
        System.out.println(log);
    }

    @Test
    public void restrictDeniesRemoteTransportByIPv6CIDRAndLogsWarning() {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DefaultAgentAuditPolicy policy =
                new DefaultAgentAuditPolicy(networkIPv6ByCIDRReadOnlyWorkingDirProfile(), AgentAuditLevel.RESTRICT, out);
        // IPv6 peer within allowed CIDR is permitted
        assertTrue(policy.transportAudit(receive("[2001:db8::1]/45000")));
        // remote peer outside allowed CIDR is denied under REMOTE_NETWORK_LIMITED + RESTRICT
        assertFalse(policy.transportAudit(receive("[2001:db9::1]/12345")));
        String log = new String(out.toByteArray(), StandardCharsets.UTF_8);
        assertTrue(log.contains("permitted"));
        assertTrue(log.contains("DENIED"));
        assertTrue(log.contains("TRANSPORT"));
        System.out.println(log);
    }

    @Test
    public void warnAllowsButReportsViolation() {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DefaultAgentAuditPolicy policy =
                new DefaultAgentAuditPolicy(localReadOnlyWorkingDirProfile(), AgentAuditLevel.WARN, out);
        // remote peer violates policy but WARN does not deny
        assertTrue(policy.transportAudit(receive("8.8.8.8/12345")));
        String log = new String(out.toByteArray(), StandardCharsets.UTF_8);
        assertTrue(log.contains("WARN"));
        assertTrue(log.contains("policy violation"));
    }

    @Test
    public void fileWorkingDirectoryEnforcement() {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DefaultAgentAuditPolicy policy =
                new DefaultAgentAuditPolicy(localReadOnlyWorkingDirProfile(), AgentAuditLevel.RESTRICT, out);
        File inWorkingDir = new File(System.getProperty("user.dir"), "agent-state.cfg");
        File outsideWorkingDir = new File("/etc/passwd");
        assertTrue(policy.fileAudit(new FileAuditEvent(this, FileAuditEvent.FileOperation.WRITE, inWorkingDir)));
        assertFalse(policy.fileAudit(new FileAuditEvent(this, FileAuditEvent.FileOperation.WRITE, outsideWorkingDir)));
    }

    @Test
    public void readOnlyFileAccessDeniesWrite() {
        AgentPolicyProfile readOnly = new AgentPolicyProfile(
                AgentPolicyProfile.ManagedObjectAccess.READ,
                AgentPolicyProfile.NetworkAccess.REMOTE_NETWORK_UNLIMITED,
                AgentPolicyProfile.FileAccess.WORKING_DIRECTORY_READONLY,
                SecurityLevel.noAuthNoPriv);
        DefaultAgentAuditPolicy policy = new DefaultAgentAuditPolicy(readOnly, AgentAuditLevel.RESTRICT);
        File anyFile = new File("/var/tmp/whatever.cfg");
        assertFalse(policy.fileAudit(new FileAuditEvent(this, FileAuditEvent.FileOperation.READ, anyFile)));
        assertFalse(policy.fileAudit(new FileAuditEvent(this, FileAuditEvent.FileOperation.WRITE, anyFile)));
        assertFalse(policy.fileAudit(new FileAuditEvent(this, FileAuditEvent.FileOperation.CREATE, anyFile)));
        assertFalse(policy.fileAudit(new FileAuditEvent(this, FileAuditEvent.FileOperation.DELETE, anyFile)));
    }

    @Test
    public void readOnlyFileAccessDeniesWriteWithinWorkingDirectory() {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DefaultAgentAuditPolicy policy =
                new DefaultAgentAuditPolicy(workingDirectoryReadOnlyProfile(), AgentAuditLevel.RESTRICT, out);
        File inWorkingDir = new File(System.getProperty("user.dir"), "agent-state.cfg");
        // reading within the working directory is the only operation a read-only profile permits
        assertTrue(policy.fileAudit(new FileAuditEvent(this, FileAuditEvent.FileOperation.READ, inWorkingDir)));
        // a modifying operation must be denied even within the working directory
        assertFalse(policy.fileAudit(new FileAuditEvent(this, FileAuditEvent.FileOperation.WRITE, inWorkingDir)));
        assertFalse(policy.fileAudit(new FileAuditEvent(this, FileAuditEvent.FileOperation.CREATE, inWorkingDir)));
        assertFalse(policy.fileAudit(new FileAuditEvent(this, FileAuditEvent.FileOperation.DELETE, inWorkingDir)));
        // reading outside of the working directory is denied as well
        assertFalse(policy.fileAudit(new FileAuditEvent(this,
                FileAuditEvent.FileOperation.READ, new File("/etc/passwd"))));
        String log = new String(out.toByteArray(), StandardCharsets.UTF_8);
        assertTrue(log.contains("permitted"));
        assertTrue(log.contains("DENIED"));
        assertTrue(log.contains("FILE"));
    }

    @Test
    public void noProfilePermitsAllButStillAudits() {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DefaultAgentAuditPolicy policy = new DefaultAgentAuditPolicy(null, AgentAuditLevel.LOG, out);
        assertTrue(policy.transportAudit(receive("8.8.8.8/12345")));
        assertTrue(policy.fileAudit(new FileAuditEvent(this, FileAuditEvent.FileOperation.DELETE, new File("/etc/passwd"))));
        String log = new String(out.toByteArray(), StandardCharsets.UTF_8);
        assertTrue(log.contains("permitted"));
        // LOG never emits warnings
        assertFalse(log.contains("WARN"));
    }

    @Test
    public void listenAndCloseAlwaysPermitted() {
        DefaultAgentAuditPolicy policy =
                new DefaultAgentAuditPolicy(localReadOnlyWorkingDirProfile(), AgentAuditLevel.RESTRICT);
        assertTrue(policy.transportAudit(new TransportAuditEvent(this,
                TransportAuditEvent.TransportOperation.LISTEN, null, new UdpAddress("0.0.0.0/161"), null)));
        assertTrue(policy.transportAudit(new TransportAuditEvent(this,
                TransportAuditEvent.TransportOperation.CLOSE, null, new UdpAddress("0.0.0.0/161"), null)));
    }
}
