Files
Liza Tsibur 2bd5475e76 Installer and Import Logic Updates (#90)
* Disable permission flag checks on imported logs (#86)

* Disable permission flag checks on imported logs

* Display individual log errors even if there are no viable logs

---------

Co-authored-by: Naomi Kramer <naomi@activecountermeasures.com>

* Installer Import Logic Updates (#89)

* rename sshprep to sshprep.sh

* installer updates

removed mass upgrades, cleaned up installer, removed references to unsupported OS's, improved error handling and usage messages

* threat intel feed error handling

* cleaned up installer scripts and improved helper functions

* Update ansible-installer.sh

---------

Co-authored-by: Naomi Kramer <naomi@activecountermeasures.com>
2026-01-20 17:50:56 -07:00

435 lines
11 KiB
Go

package util
import (
"context"
"crypto/md5" // #nosec
"database/sql/driver"
"encoding/hex"
"errors"
"fmt"
"math"
"net"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"github.com/blang/semver"
"github.com/google/go-github/github"
"github.com/google/uuid"
"github.com/spf13/afero"
)
var (
privateIPBlocks []Subnet
PublicNetworkUUID = uuid.MustParse("ffffffff-ffff-ffff-ffff-ffffffffffff")
PublicNetworkName = "Public"
UnknownPrivateNetworkUUID = uuid.MustParse("ffffffff-ffff-ffff-ffff-fffffffffffe")
UnknownPrivateNetworkName = "Unknown Private"
ErrFileSystemIsNil = errors.New("filesystem is nil")
ErrInvalidPath = errors.New("path cannot be empty string")
ErrFileDoesNotExist = errors.New("file does not exist")
ErrFileIsEmtpy = errors.New("file is empty")
ErrPathIsDir = errors.New("given path is a directory, not a file")
ErrDirDoesNotExist = errors.New("directory does not exist")
ErrDirIsEmpty = errors.New("directory is empty")
ErrPathIsNotDir = errors.New("given path is not a directory")
ErrFetchingLatestRelease = errors.New("error fetching latest release")
ErrParsingCurrentVersion = errors.New("error parsing current version")
ErrParsingLatestVersion = errors.New("error parsing latest version")
// aliased for mocking in tests
readFile = afero.ReadFile
pathExists = afero.Exists
isDirectory = afero.IsDir
isEmpty = afero.IsEmpty
getUserHomeDir = os.UserHomeDir
getWorkingDir = os.Getwd
)
type FixedString struct {
val string
Data [16]byte
}
type WalkError struct {
Path string
Error error
}
func init() {
// parse private IPs
privateIPs, _ := NewSubnetList(
[]string{
// "127.0.0.0/8", // IPv4 Loopback; handled by ip.IsLoopback
// "::1/128", // IPv6 Loopback; handled by ip.IsLoopback
// "169.254.0.0/16", // RFC3927 link-local; handled by ip.IsLinkLocalUnicast()
// "fe80::/10", // IPv6 link-local; handled by ip.IsLinkLocalUnicast()
"10.0.0.0/8", // RFC1918
"172.16.0.0/12", // RFC1918
"192.168.0.0/16", // RFC1918
"fc00::/7", // IPv6 unique local addr
})
// if err != nil {
// // TODO: should we replace this panic with something else?
// panic(fmt.Sprintf("error defining private IPs: %v", err.Error()))
// }
privateIPBlocks = privateIPs
}
// NewFixedStringHash creates a FixedString from a hash of all the passed in strings
func NewFixedStringHash(args ...string) (FixedString, error) {
if len(args) == 0 {
return FixedString{}, errors.New("no arguments provided")
}
joined := strings.Join(args, "")
if joined == "" {
return FixedString{}, errors.New("joined string is empty")
}
// #nosec
hash := md5.Sum([]byte(strings.Join(args, "")))
fs := FixedString{
Data: hash,
}
return fs, nil
}
// NewFixedStringFromString creates a FixedString from a passed in hex string
func NewFixedStringFromHex(h string) (FixedString, error) {
if h == "" {
return FixedString{}, errors.New("hex string is empty")
}
data, err := hex.DecodeString(h)
if err != nil {
return FixedString{}, fmt.Errorf("error decoding hex string: %w", err)
}
var fixed [16]byte
copy(fixed[:], data)
return FixedString{
Data: fixed,
}, nil
}
func (bin *FixedString) Hex() string {
return strings.ToUpper(hex.EncodeToString(bin.Data[:]))
}
// override functions called by database driver
// Returns expected type for writing to the database
func (bin FixedString) MarshalBinary() ([]byte, error) {
return bin.Data[:], nil
}
// Returns expected type for reading from the database
func (bin *FixedString) UnmarshalBinary(b []byte) error {
copy(bin.Data[:], b)
return nil
}
// Returns value of FixedString as a pointer, used when sometimes writing to database
func (bin FixedString) Value() (driver.Value, error) {
return &bin.val, nil
}
func ValidFQDN(value string) bool {
// Regular expression for validating FQDN
// This pattern requires at least two labels (separated by dots), with each label starting and ending with an alphanumeric character.
// Labels in between can have hyphens. The last label (TLD) must be at least two characters long, with only letters.
re := regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`)
return re.MatchString(value)
}
// ContainsIP checks if a collection of subnets contains an IP
func ContainsIP(subnets []Subnet, ip net.IP) bool {
// cache IPv4 conversion so it not performed every in every Contains call
if ipv4 := ip.To4(); ipv4 != nil {
ip = ipv4
}
for _, block := range subnets {
if block.Contains(ip) {
return true
}
}
return false
}
// IPIsPubliclyRoutable checks if an IP address is publicly routable
func IPIsPubliclyRoutable(ip net.IP) bool {
// check if the IP is a loopback, link-local unicast, or link-local multicast address
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
return false
}
// check if the IP is in the user-defined private IP blocks
if ContainsIP(privateIPBlocks, ip) {
return false
}
return true
}
// ParseNetworkID returns the network ID for a given IP address and agent ID
func ParseNetworkID(ip net.IP, agentID string) uuid.UUID {
if IPIsPubliclyRoutable(ip) {
return PublicNetworkUUID
}
if len(agentID) == 0 {
return UnknownPrivateNetworkUUID
}
networkID, err := uuid.Parse(agentID)
if err != nil {
return UnknownPrivateNetworkUUID
}
return networkID
}
// ContainsDomain checks if a given host is in a list of domains
func ContainsDomain(domains []string, host string) bool {
for _, entry := range domains {
// check for wildcard
if strings.Contains(entry, "*") {
// trim asterisk from the wildcard domain
wildcardDomain := strings.TrimPrefix(entry, "*")
// This would match a.mydomain.com, b.mydomain.com etc.,
if strings.HasSuffix(host, wildcardDomain) {
return true
}
// check match of top domain of wildcard
// if a user added *.mydomain.com, this will include mydomain.com
// in the filtering
wildcardDomain = strings.TrimPrefix(wildcardDomain, ".")
if host == wildcardDomain {
return true
}
// match on exact
} else if host == entry {
return true
}
}
return false
}
// UInt32sAreSorted returns true if a slice of uint32 is sorted in ascending order
func UInt32sAreSorted(data []uint32) bool {
return sort.SliceIsSorted(data, func(i, j int) bool { return data[i] < data[j] })
}
// SortUInt32s sorts a slice of uint32 in ascending order
func SortUInt32s(data []uint32) {
sort.Slice(data, func(i, j int) bool { return data[i] < data[j] })
}
func ValidateTimestamp(timestamp time.Time) (time.Time, bool) {
if timestamp.UTC().Unix() > 0 && timestamp.UTC().Unix() < math.MaxInt64 {
return timestamp, false
}
return time.Unix(0, 1), true
}
// GetRelativeFirstSeenTimestamp returns the timestamp to use for first seen calculation/display.
// This is a shortcut for a commonly used if statement
func GetRelativeFirstSeenTimestamp(useCurrentTime bool, maxTimestamp time.Time) time.Time {
if !useCurrentTime {
// use the max timestamp to score against
return maxTimestamp
}
return time.Now()
}
// ParseRelativePath parses a given directory path and returns the absolute path
func ParseRelativePath(dir string) (string, error) {
// validate parameters
if dir == "" {
return "", ErrInvalidPath
}
switch {
// if path is home, parse and set home dir
case dir[:2] == "~/":
home, err := getUserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, dir[2:]), nil
// if the path starts with a dot, get the path relative to the current working directory
case strings.HasPrefix(dir, "."):
currentDir, err := getWorkingDir()
if err != nil {
return "", err
}
return filepath.Join(currentDir, dir), nil
default:
// otherwise, return the directory as is
return dir, nil
}
}
// ValidateDirectory returns whether a directory exists and is empty
func ValidateDirectory(afs afero.Fs, dir string) error {
// validate path
exists, isDir, empty, err := validatePath(afs, dir)
if err != nil {
return err
}
// check if dirctory exists
if !exists {
return fmt.Errorf("%w: %s", ErrDirDoesNotExist, dir)
}
// check if path is a directory
if !isDir {
return fmt.Errorf("%w: %s", ErrPathIsNotDir, dir)
}
// check if file is empty
if empty {
return fmt.Errorf("%w: %s", ErrDirIsEmpty, dir)
}
return nil
}
// Validate File
func ValidateFile(afs afero.Fs, file string) error {
// validate path
exists, isDir, empty, err := validatePath(afs, file)
if err != nil {
return err
}
// check if file exists
if !exists {
return fmt.Errorf("%w: %s", ErrFileDoesNotExist, file)
}
// check if path is a directory
if isDir {
return fmt.Errorf("%w: %s", ErrPathIsDir, file)
}
// check if file is empty
if empty {
return fmt.Errorf("%w: %s", ErrFileIsEmtpy, file)
}
return nil
}
// validatePath validates a given path
func validatePath(afs afero.Fs, path string) (bool, bool, bool, error) {
var exists, isDir, empty bool
// validate parameters
if afs == nil {
return exists, isDir, empty, ErrFileSystemIsNil
}
if path == "" {
return exists, isDir, empty, ErrInvalidPath
}
// check if path exists
exists, err := pathExists(afs, path)
if err != nil {
return exists, isDir, empty, err
}
if exists {
// check if path is a directory
isDir, err = isDirectory(afs, path)
if err != nil {
return exists, isDir, empty, err
}
// check if directory is empty
empty, err = isEmpty(afs, path)
if err != nil {
return exists, isDir, empty, err
}
}
return exists, isDir, empty, nil
}
// GetFileContents reads the contents of a file at the specified path
func GetFileContents(afs afero.Fs, path string) ([]byte, error) {
// validate file
err := ValidateFile(afs, path)
if err != nil {
return nil, err
}
// get file contents
file, err := readFile(afs, path)
if err != nil {
return nil, err
}
return file, nil
}
// CheckForNewerVersion checks if a newer version of the project is available on the GitHub repository
func CheckForNewerVersion(client *github.Client, currentVersion string) (bool, string, error) {
// get the latest version
latestVersion, err := GetLatestReleaseVersion(client, "activecm", "rita")
if err != nil {
return false, "", err
}
// parse the current version
currentSemver, err := semver.ParseTolerant(currentVersion)
if err != nil {
return false, "", fmt.Errorf("%w: %w", ErrParsingCurrentVersion, err)
}
// parse the latest version
latestSemver, err := semver.ParseTolerant(latestVersion)
if err != nil {
return false, "", fmt.Errorf("%w: %w", ErrParsingLatestVersion, err)
}
// compare the versions
if latestSemver.GT(currentSemver) {
return true, latestVersion, nil
}
return false, latestVersion, nil
}
// GetLatestReleaseVersion gets the latest release version from the GitHub repository
func GetLatestReleaseVersion(client *github.Client, owner, repo string) (string, error) {
// get the latest release
latestRelease, _, err := client.Repositories.GetLatestRelease(context.Background(), owner, repo)
if err != nil {
return "", fmt.Errorf("%w: %w", ErrFetchingLatestRelease, err)
}
// get the latest version from release tag name
latestVersion := latestRelease.GetTagName()
return latestVersion, nil
}