mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-09-11 04:07:47 +02:00
- Add ssh_exec() and ssh_rsync() functions for remote command execution and file transfer with connection pooling - Add db_import_dns_asset() to import DNS zone records and group by domain with A/AAAA records stored separately - Add db_import_custom_asset() for flexible JSONL asset import with direct field mapping, supporting tags and custom asset types - Extend Asset model with ExternalURL, Remarks (string array), Language, Size, and LOC fields for better metadata tracking - Add backward compatibility layer for legacy JSON formats (string remarks, tags array merging) - Register new functions in function registry with proper documentation and usage examples
277 lines
8.2 KiB
Go
277 lines
8.2 KiB
Go
package functions
|
|
|
|
import (
|
|
"regexp"
|
|
"sync"
|
|
|
|
"github.com/dop251/goja"
|
|
)
|
|
|
|
// Variable extraction for lazy loading optimization
|
|
var (
|
|
// varRefCache caches parsed variable references per expression
|
|
varRefCache sync.Map // expr -> []string
|
|
|
|
// compiledCache caches compiled JavaScript programs for reuse.
|
|
// This avoids reparsing the same expression in foreach loops with 1000+ items.
|
|
compiledCache sync.Map // expr -> *goja.Program
|
|
|
|
// Pattern to match variable identifiers (excludes JS keywords)
|
|
varPattern = regexp.MustCompile(`\b([a-zA-Z_][a-zA-Z0-9_]*)\b`)
|
|
|
|
// JavaScript keywords and built-in objects to exclude from variable extraction
|
|
jsKeywords = map[string]bool{
|
|
// Keywords
|
|
"true": true, "false": true, "null": true, "undefined": true,
|
|
"if": true, "else": true, "for": true, "while": true, "do": true,
|
|
"switch": true, "case": true, "default": true, "break": true, "continue": true,
|
|
"function": true, "return": true, "var": true, "let": true, "const": true,
|
|
"new": true, "delete": true, "typeof": true, "instanceof": true,
|
|
"this": true, "void": true, "in": true, "of": true,
|
|
"try": true, "catch": true, "finally": true, "throw": true,
|
|
"class": true, "extends": true, "super": true, "import": true, "export": true,
|
|
"async": true, "await": true, "yield": true,
|
|
// Built-in objects
|
|
"Math": true, "String": true, "Number": true, "Boolean": true,
|
|
"Array": true, "Object": true, "JSON": true, "Date": true,
|
|
"RegExp": true, "Error": true, "console": true, "parseInt": true, "parseFloat": true,
|
|
"isNaN": true, "isFinite": true, "encodeURI": true, "decodeURI": true,
|
|
"encodeURIComponent": true, "decodeURIComponent": true,
|
|
}
|
|
)
|
|
|
|
// extractVariables returns variable names referenced in expression.
|
|
// Results are cached for repeated expressions (common in loop iterations).
|
|
func extractVariables(expr string) []string {
|
|
if cached, ok := varRefCache.Load(expr); ok {
|
|
return cached.([]string)
|
|
}
|
|
|
|
matches := varPattern.FindAllStringSubmatch(expr, -1)
|
|
seen := make(map[string]bool)
|
|
var vars []string
|
|
|
|
for _, match := range matches {
|
|
name := match[1]
|
|
if !seen[name] && !jsKeywords[name] {
|
|
seen[name] = true
|
|
vars = append(vars, name)
|
|
}
|
|
}
|
|
|
|
varRefCache.Store(expr, vars)
|
|
return vars
|
|
}
|
|
|
|
// getCompiledProgram returns a cached compiled program for the expression,
|
|
// compiling it on first access. This provides 60-80% faster loop condition
|
|
// evaluation by avoiding reparsing the same expression multiple times.
|
|
func getCompiledProgram(expr string) (*goja.Program, error) {
|
|
if cached, ok := compiledCache.Load(expr); ok {
|
|
return cached.(*goja.Program), nil
|
|
}
|
|
prg, err := goja.Compile("condition", expr, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
compiledCache.Store(expr, prg)
|
|
return prg, nil
|
|
}
|
|
|
|
// vmContextRegistry maps Goja VMs to their execution context.
|
|
// This allows functions to find their context via the VM reference.
|
|
var vmContextRegistry sync.Map // map[*goja.Runtime]*VMContext
|
|
|
|
// getVMContext retrieves the execution context for a given Goja VM.
|
|
// Returns nil if no context is registered (shouldn't happen in normal use).
|
|
func getVMContext(vm *goja.Runtime) *VMContext {
|
|
if ctx, ok := vmContextRegistry.Load(vm); ok {
|
|
return ctx.(*VMContext)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// VMContext wraps a Goja VM with per-execution context fields.
|
|
// This allows parallel execution by giving each goroutine its own VM+context.
|
|
type VMContext struct {
|
|
vm *goja.Runtime
|
|
|
|
// Context fields for function execution (previously on GojaRuntime)
|
|
workspaceName string
|
|
stateFile string
|
|
scanID string // RunUUID (string identifier)
|
|
runID int64 // Run.ID (integer for database foreign keys)
|
|
workflowName string
|
|
workflowKind string
|
|
target string
|
|
workspacePath string
|
|
|
|
// RuntimeVars stores variables set via set_var() for retrieval with get_var()
|
|
RuntimeVars map[string]string
|
|
|
|
// suppressDetails suppresses verbose console output (propagated from step's suppress_details)
|
|
suppressDetails bool
|
|
}
|
|
|
|
// VMRegistrationFunc is called to register functions on a new VM
|
|
type VMRegistrationFunc func(vm *goja.Runtime)
|
|
|
|
// VMPool provides goroutine-safe access to configured Goja VMs.
|
|
// Uses sync.Pool for efficient VM reuse.
|
|
type VMPool struct {
|
|
pool sync.Pool
|
|
registerFn VMRegistrationFunc
|
|
}
|
|
|
|
// NewVMPool creates a new VM pool with a function registration callback.
|
|
// The registerFn is called once per VM to register built-in functions.
|
|
func NewVMPool(registerFn VMRegistrationFunc) *VMPool {
|
|
p := &VMPool{
|
|
registerFn: registerFn,
|
|
}
|
|
p.pool = sync.Pool{
|
|
New: func() interface{} {
|
|
ctx := newVMContext()
|
|
// Register functions on the new VM
|
|
if p.registerFn != nil {
|
|
p.registerFn(ctx.vm)
|
|
}
|
|
return ctx
|
|
},
|
|
}
|
|
return p
|
|
}
|
|
|
|
// Get retrieves a VM context from the pool and registers it
|
|
func (p *VMPool) Get() *VMContext {
|
|
ctx := p.pool.Get().(*VMContext)
|
|
// Register the VM->context mapping so functions can find their context
|
|
vmContextRegistry.Store(ctx.vm, ctx)
|
|
return ctx
|
|
}
|
|
|
|
// Put clears context fields, unregisters the VM, and returns context to pool
|
|
func (p *VMPool) Put(ctx *VMContext) {
|
|
// Unregister the VM->context mapping
|
|
vmContextRegistry.Delete(ctx.vm)
|
|
|
|
// Clear context fields to prevent data leakage between executions
|
|
ctx.workspaceName = ""
|
|
ctx.stateFile = ""
|
|
ctx.scanID = ""
|
|
ctx.runID = 0
|
|
ctx.workflowName = ""
|
|
ctx.workflowKind = ""
|
|
ctx.target = ""
|
|
ctx.workspacePath = ""
|
|
ctx.RuntimeVars = nil
|
|
ctx.suppressDetails = false
|
|
|
|
p.pool.Put(ctx)
|
|
}
|
|
|
|
// newVMContext creates a new Goja VM (functions will be registered by caller)
|
|
func newVMContext() *VMContext {
|
|
return &VMContext{
|
|
vm: goja.New(),
|
|
}
|
|
}
|
|
|
|
// VM returns the underlying Goja VM for function registration
|
|
func (v *VMContext) VM() *goja.Runtime {
|
|
return v.vm
|
|
}
|
|
|
|
// SetContext sets the execution context from a template context map
|
|
func (v *VMContext) SetContext(ctx map[string]interface{}) {
|
|
// Extract workspace from context
|
|
if ws, ok := ctx["Workspace"].(string); ok {
|
|
v.workspaceName = ws
|
|
} else if ws, ok := ctx["TargetSpace"].(string); ok {
|
|
v.workspaceName = ws
|
|
}
|
|
|
|
// Extract state file path
|
|
if sf, ok := ctx["StateFile"].(string); ok {
|
|
v.stateFile = sf
|
|
}
|
|
|
|
// Extract scan ID (RunUUID) - check both new and legacy variable names
|
|
if sid, ok := ctx["RunUUID"].(string); ok {
|
|
v.scanID = sid
|
|
} else if sid, ok := ctx["TaskID"].(string); ok {
|
|
v.scanID = sid
|
|
}
|
|
|
|
// Extract database Run.ID (integer for foreign keys)
|
|
if rid, ok := ctx["DBRunID"].(int64); ok {
|
|
v.runID = rid
|
|
}
|
|
|
|
// Extract workflow name and kind
|
|
if wn, ok := ctx["WorkflowName"].(string); ok {
|
|
v.workflowName = wn
|
|
}
|
|
if wk, ok := ctx["WorkflowKind"].(string); ok {
|
|
v.workflowKind = wk
|
|
}
|
|
|
|
// Extract target
|
|
if t, ok := ctx["Target"].(string); ok {
|
|
v.target = t
|
|
}
|
|
|
|
// Extract workspace path (Output directory)
|
|
if op, ok := ctx["Output"].(string); ok {
|
|
v.workspacePath = op
|
|
}
|
|
|
|
// Extract suppress details flag (from step's suppress_details)
|
|
if sd, ok := ctx["SuppressDetails"].(bool); ok {
|
|
v.suppressDetails = sd
|
|
}
|
|
}
|
|
|
|
// SetVariables sets context variables on the VM
|
|
func (v *VMContext) SetVariables(ctx map[string]interface{}) error {
|
|
for k, val := range ctx {
|
|
if err := v.vm.Set(k, val); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetVariablesLazy sets only variables referenced in the expression.
|
|
// This is an optimization for expressions that use only a few variables
|
|
// from a large context (50-80% faster for typical pre_condition checks).
|
|
func (v *VMContext) SetVariablesLazy(ctx map[string]interface{}, expr string) error {
|
|
referenced := extractVariables(expr)
|
|
|
|
for _, name := range referenced {
|
|
if val, ok := ctx[name]; ok {
|
|
if err := v.vm.Set(name, val); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Run executes a JavaScript expression using a precompiled program if available.
|
|
// Compiled programs are cached for reuse, providing 60-80% faster evaluation
|
|
// in foreach loops where the same condition is evaluated 1000+ times.
|
|
func (v *VMContext) Run(expr string) (goja.Value, error) {
|
|
prg, err := getCompiledProgram(expr)
|
|
if err != nil {
|
|
// Fallback to direct execution if compilation fails
|
|
return v.vm.RunString(expr)
|
|
}
|
|
return v.vm.RunProgram(prg)
|
|
}
|
|
|
|
// ToValue converts a Go value to a Goja value
|
|
func (v *VMContext) ToValue(val interface{}) goja.Value {
|
|
return v.vm.ToValue(val)
|
|
}
|