mirror of
https://github.com/activecm/rita
synced 2026-06-08 13:02:45 +00:00
2bd5475e76
* 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>
435 lines
11 KiB
Go
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
|
|
}
|