Files
OliveTin/service/internal/executor/arguments.go
2025-10-26 16:51:24 +00:00

362 lines
8.6 KiB
Go

package executor
import (
config "github.com/OliveTin/OliveTin/internal/config"
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
log "github.com/sirupsen/logrus"
"fmt"
"net/mail"
"net/url"
"regexp"
"strings"
"time"
)
var (
typecheckRegex = map[string]string{
"very_dangerous_raw_string": "",
"int": "^[\\d]+$",
"unicode_identifier": "^[\\w\\/\\\\.\\_ \\d]+$",
"ascii": "^[a-zA-Z0-9]+$",
"ascii_identifier": "^[a-zA-Z0-9\\-\\.\\_]+$",
"ascii_sentence": "^[a-zA-Z0-9 \\,\\.]+$",
}
)
func parseCommandForReplacements(shellCommand string, values map[string]string) (string, error) {
r := regexp.MustCompile("{{ *?([a-zA-Z0-9_]+?) *?}}")
foundArgumentNames := r.FindAllStringSubmatch(shellCommand, -1)
for _, match := range foundArgumentNames {
argName := match[1]
argValue, argProvided := values[argName]
if !argProvided {
return "", fmt.Errorf("required arg not provided: %v", argName)
}
shellCommand = strings.ReplaceAll(shellCommand, match[0], argValue)
}
return shellCommand, nil
}
func parseActionArguments(values map[string]string, action *config.Action, entityPrefix string) (string, error) {
log.WithFields(log.Fields{
"actionTitle": action.Title,
"cmd": action.Shell,
}).Infof("Action parse args - Before")
for _, arg := range action.Arguments {
argName := arg.Name
argValue := values[argName]
err := typecheckActionArgument(&arg, argValue, action)
if err != nil {
return "", err
}
log.WithFields(log.Fields{
"name": argName,
"value": argValue,
}).Debugf("Arg assigned")
}
parsedShellCommand, err := parseCommandForReplacements(action.Shell, values)
parsedShellCommand = sv.ReplaceEntityVars(entityPrefix, parsedShellCommand)
redactedShellCommand := redactShellCommand(parsedShellCommand, action.Arguments, values)
if err != nil {
return "", err
}
log.WithFields(log.Fields{
"actionTitle": action.Title,
"cmd": redactedShellCommand,
}).Infof("Action parse args - After")
return parsedShellCommand, nil
}
//gocyclo:ignore
func redactShellCommand(shellCommand string, arguments []config.ActionArgument, argumentValues map[string]string) string {
for _, arg := range arguments {
if arg.Type == "password" {
argValue, exists := argumentValues[arg.Name]
if !exists {
log.Warnf("Redact shell command: Argument %s not found in values", arg.Name)
continue
}
if argValue == "" {
continue
}
shellCommand = strings.ReplaceAll(shellCommand, argValue, "<redacted>")
}
}
return shellCommand
}
func typecheckActionArgument(arg *config.ActionArgument, value string, action *config.Action) error {
if arg.Type == "confirmation" {
return nil
}
if arg.Name == "" {
return fmt.Errorf("argument name cannot be empty")
}
return typecheckActionArgumentFound(value, action, arg)
}
func typecheckActionArgumentFound(value string, action *config.Action, arg *config.ActionArgument) error {
if value == "" {
return typecheckNull(arg)
}
if len(arg.Choices) > 0 {
return typecheckChoice(value, arg)
}
return TypeSafetyCheck(arg.Name, value, arg.Type)
}
// TypeSafetyCheck checks argument values match a specific type. The types are
// defined in typecheckRegex, and, you guessed it, uses regex to check for allowed
// characters.
//
//gocyclo:ignore
func TypeSafetyCheck(name string, value string, argumentType string) error {
switch argumentType {
case "password":
return nil
case "raw_string_multiline":
return nil
case "email":
return typeSafetyCheckEmail(value)
case "url":
return typeSafetyCheckUrl(value)
case "datetime":
return typeSafetyCheckDatetime(value)
}
return typeSafetyCheckRegex(name, value, argumentType)
}
func typecheckNull(arg *config.ActionArgument) error {
if arg.RejectNull {
return fmt.Errorf("null values are not allowed")
}
return nil
}
func typecheckChoice(value string, arg *config.ActionArgument) error {
if arg.Entity != "" {
return typecheckChoiceEntity(value, arg)
}
for _, choice := range arg.Choices {
if value == choice.Value {
return nil
}
}
return fmt.Errorf("argument value is not one of the predefined choices")
}
func typecheckChoiceEntity(value string, arg *config.ActionArgument) error {
templateChoice := arg.Choices[0].Value
for _, ent := range sv.GetEntities(arg.Entity) {
choice := sv.ReplaceEntityVars(ent, templateChoice)
if value == choice {
return nil
}
}
return fmt.Errorf("argument value cannot be found in entities")
}
func typeSafetyCheckEmail(value string) error {
_, err := mail.ParseAddress(value)
log.Errorf("Email check: %v, %v", err, value)
if err != nil {
return err
}
return nil
}
func typeSafetyCheckDatetime(value string) error {
_, err := time.Parse("2006-01-02T15:04:05", value)
if err != nil {
return err
}
return nil
}
func typeSafetyCheckRegex(name string, value string, argumentType string) error {
pattern := ""
if strings.HasPrefix(argumentType, "regex:") {
pattern = strings.Replace(argumentType, "regex:", "", 1)
} else {
found := false
pattern, found = typecheckRegex[argumentType]
if !found {
return fmt.Errorf("argument type not implemented %v for arg: %v", argumentType, name)
}
}
matches, _ := regexp.MatchString(pattern, value)
if !matches {
log.WithFields(log.Fields{
"name": name,
"value": value,
"type": argumentType,
"pattern": pattern,
}).Warn("Arg type check safety failure")
return fmt.Errorf("invalid argument %v, doesn't match %v", name, argumentType)
}
return nil
}
func typeSafetyCheckUrl(value string) error {
_, err := url.ParseRequestURI(value)
return err
}
func mangleInvalidArgumentValues(req *ExecutionRequest) {
for _, arg := range req.Action.Arguments {
if arg.Type == "datetime" {
mangleInvalidDatetimeValues(req, &arg)
}
mangleCheckboxValues(req, &arg)
}
}
func mangleCheckboxValues(req *ExecutionRequest, arg *config.ActionArgument) {
if arg.Type != "checkbox" {
return
}
log.Infof("Checking checkbox values for argument %s in action %s", arg.Name, req.Action.Title)
for i, _ := range arg.Choices {
choice := &arg.Choices[i]
if req.Arguments[arg.Name] == choice.Title {
log.WithFields(log.Fields{
"arg": arg.Name,
"oldValue": req.Arguments[arg.Name],
"newValue": choice.Value,
"actionTitle": req.Action.Title,
}).Infof("Mangled checkbox value")
req.Arguments[arg.Name] = choice.Value
}
}
}
func mangleInvalidDatetimeValues(req *ExecutionRequest, arg *config.ActionArgument) {
value, exists := req.Arguments[arg.Name]
if !exists || value == "" {
return
}
timestamp, err := time.Parse("2006-01-02T15:04", value)
if err == nil {
log.WithFields(log.Fields{
"arg": arg.Name,
"value": value,
"actionTitle": req.Action.Title,
}).Warnf("Mangled invalid datetime value without seconds to :00 seconds, this issue is commonly caused by Android browsers.")
req.Arguments[arg.Name] = timestamp.Format("2006-01-02T15:04:05")
}
}
func parseActionExec(values map[string]string, action *config.Action, entityPrefix string) ([]string, error) {
for _, arg := range action.Arguments {
argName := arg.Name
argValue := values[argName]
err := typecheckActionArgument(&arg, argValue, action)
if err != nil {
return nil, err
}
log.WithFields(log.Fields{
"name": argName,
"value": argValue,
}).Debugf("Arg assigned")
}
parsedArgs := make([]string, len(action.Exec))
for i, arg := range action.Exec {
parsedArg, err := parseCommandForReplacements(arg, values)
if err != nil {
return nil, err
}
parsedArg = sv.ReplaceEntityVars(entityPrefix, parsedArg)
parsedArgs[i] = parsedArg
}
redactedArgs := redactExecArgs(parsedArgs, action.Arguments, values)
log.WithFields(log.Fields{
"actionTitle": action.Title,
"cmd": redactedArgs,
}).Infof("Action parse args - After (Exec)")
return parsedArgs, nil
}
//gocyclo:ignore
func redactExecArgs(execArgs []string, arguments []config.ActionArgument, argumentValues map[string]string) []string {
redacted := make([]string, len(execArgs))
for i, arg := range execArgs {
redacted[i] = redactShellCommand(arg, arguments, argumentValues)
}
return redacted
}
func checkShellArgumentSafety(action *config.Action) error {
if action.Shell == "" {
return nil
}
unsafeTypes := []string{"url", "email", "raw_string_multiline", "very_dangerous_raw_string"}
for _, arg := range action.Arguments {
for _, unsafeType := range unsafeTypes {
if arg.Type == unsafeType {
return fmt.Errorf("unsafe argument type '%s' cannot be used with Shell execution. Use 'exec' instead. See https://docs.olivetin.app/action_execution/shellvsexec.html", arg.Type)
}
}
}
return nil
}