// apparmor.d - Full set of apparmor profiles
// Copyright (C) 2021-2024 Alexandre Pujol <alexandre@pujol.io>
// SPDX-License-Identifier: GPL-2.0-only

package logs

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/roddhjav/apparmor.d/pkg/util"
)

// LogFiles is the list of default path to query
var LogFiles = []string{
	"/var/log/audit/audit.log",
	"/var/log/syslog",
}

// SystemdLog is a simplified systemd json log representation.
type systemdLog struct {
	Message string `json:"MESSAGE"`
}

// GetApparmorLogs return a list of cleaned apparmor logs from a file
func GetApparmorLogs(file io.Reader, profile string) []string {
	var logs []string

	isAppArmorLog := isAppArmorLogTemplate.Copy()
	if profile != "" {
		exp := `apparmor=("DENIED"|"ALLOWED"|"AUDIT")`
		exp = fmt.Sprintf(exp+`.* (profile="%s.*"|label="%s.*")`, profile, profile)
		isAppArmorLog = regexp.MustCompile(exp)
	}

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := scanner.Text()
		if isAppArmorLog.MatchString(line) {
			logs = append(logs,
				regCleanLogs.Replace(util.DecodeHexInString(line)),
			)
		}
	}
	return util.RemoveDuplicate(logs)
}

// GetAuditLogs return a reader with the logs entries from Auditd
func GetAuditLogs(path string) (io.Reader, error) {
	file, err := os.Open(filepath.Clean(path))
	if err != nil {
		return nil, err
	}
	return file, nil
}

// GetJournalctlLogs return a reader with the logs entries from Systemd
func GetJournalctlLogs(path string, useFile bool) (io.Reader, error) {
	var logs []systemdLog
	var stdout bytes.Buffer
	var scanner *bufio.Scanner

	if useFile {
		file, err := os.Open(filepath.Clean(path))
		if err != nil {
			return nil, err
		}
		scanner = bufio.NewScanner(file)
	} else {
		// journalctl -b -o json -g apparmor -t kernel -t audit -t dbus-daemon --output-fields=MESSAGE > systemd.log
		args := []string{
			"--boot", "--grep=apparmor",
			"--identifier=kernel", "--identifier=audit", "--identifier=dbus-daemon",
			"--output=json", "--output-fields=MESSAGE",
		}
		cmd := exec.Command("journalctl", args...)
		cmd.Stdout = &stdout
		if err := cmd.Run(); err != nil {
			return nil, err
		}
		scanner = bufio.NewScanner(&stdout)
	}

	var jctlRaw []string
	for scanner.Scan() {
		line := scanner.Text()
		if strings.Contains(line, "apparmor") {
			jctlRaw = append(jctlRaw, line)
		}
	}

	jctlStr := "[" + strings.Join(jctlRaw, ",\n") + "]"
	if err := json.Unmarshal([]byte(jctlStr), &logs); err != nil {
		return nil, err
	}

	var res strings.Builder
	for _, log := range logs {
		res.WriteString(log.Message + "\n")
	}
	return strings.NewReader(res.String()), nil
}

// SelectLogFile return the path of the available log file to parse (audit, syslog, .1, .2)
func SelectLogFile(path string) string {
	info, err := os.Stat(filepath.Clean(path))
	if err == nil && !info.IsDir() {
		return path
	}
	for _, logfile := range LogFiles {
		if _, err := os.Stat(logfile); err == nil {
			oldLogfile := filepath.Clean(logfile + "." + path)
			if _, err := os.Stat(oldLogfile); err == nil {
				return oldLogfile
			} else {
				return logfile
			}
		}
	}
	return ""
}