diff --git a/.gitignore b/.gitignore index f70370c..c54691d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ build/ .darn -library/ \ No newline at end of file +/library/ \ No newline at end of file diff --git a/README.md b/README.md index 3b9223e..1665551 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,37 @@ Executes the steps defined in a generated plan. (For more `darnit` subcommands like `parameters` and `mapping`, refer to `darnit --help`) +## Documentation + +- **[Library Quick Start](docs/LIBRARY_QUICKSTART.md)** - Get started with the library system in 5 minutes +- **[Library System Guide](docs/LIBRARY_SYSTEM.md)** - Complete documentation on how the library system works +- **[Library Management](docs/LIBRARY_MANAGEMENT.md)** - Advanced library management and troubleshooting +- **[Architecture Overview](docs/ARCHITECTURE.md)** - High-level system architecture and component interactions + +### Key Concepts + +- **Actions** - Executable operations (CLI commands, file creation) +- **Templates** - Reusable content patterns +- **Mappings** - Rules that map security findings to remediation actions +- **Library** - Organized collection of actions, templates, and mappings + +### Common Commands + +```bash +# Library management +darn library init # Initialize new library +darn library sync # Update with latest defaults +darn library diagnose # Troubleshoot issues + +# Working with actions +darn action list # List available actions +darn action show # Show action details + +# Remediation workflow +darnit plan generate # Generate remediation plan +darnit plan execute # Execute remediation plan +``` + --- ### Deprecated `darn init` diff --git a/cmd/darn/cmd/action/action.go b/cmd/darn/cmd/action/action.go index 17b5f31..481ebe5 100644 --- a/cmd/darn/cmd/action/action.go +++ b/cmd/darn/cmd/action/action.go @@ -29,6 +29,10 @@ func NewActionCmd() *cobra.Command { actionCmd.AddCommand(newActionListCmd()) actionCmd.AddCommand(newActionInfoCmd()) actionCmd.AddCommand(newActionRunCmd()) + actionCmd.AddCommand(newActionAddCmd()) + actionCmd.AddCommand(newActionValidateCmd()) + actionCmd.AddCommand(newActionSchemaCmd()) + actionCmd.AddCommand(newActionExampleCmd()) return actionCmd } @@ -207,19 +211,68 @@ func newActionInfoCmd() *cobra.Command { fmt.Printf("Arguments: %s\n", strings.Join(actionConfig.Args, " ")) } - // Display parameter schema + // Display parameter information in a user-friendly way if actionConfig.Schema != nil { - fmt.Println("\nParameter Schema:") - schemaJSON, _ := json.MarshalIndent(actionConfig.Schema, "", " ") - fmt.Println(string(schemaJSON)) - } + fmt.Println("\nParameters:") + schemaMap := actionConfig.Schema + if props, exists := schemaMap["properties"]; exists { + if properties, ok := props.(map[string]interface{}); ok { + // Get required fields + requiredFields := make(map[string]bool) + if required, exists := schemaMap["required"]; exists { + if reqArray, ok := required.([]interface{}); ok { + for _, req := range reqArray { + if reqStr, ok := req.(string); ok { + requiredFields[reqStr] = true + } + } + } + } - // Display default values if any - if len(actionConfig.Defaults) > 0 { - fmt.Println("\nDefault Values:") - for k, v := range actionConfig.Defaults { - fmt.Printf(" %s: %v\n", k, v) + // Display each parameter + for paramName, paramDef := range properties { + if def, ok := paramDef.(map[string]interface{}); ok { + fmt.Printf(" %s", paramName) + + // Show if required + if requiredFields[paramName] { + fmt.Printf(" (required)") + } else { + fmt.Printf(" (optional)") + } + + // Show type + if pType, exists := def["type"]; exists { + fmt.Printf(" [%v]", pType) + } + + fmt.Println() + + // Show description + if desc, exists := def["description"]; exists { + fmt.Printf(" %v\n", desc) + } + + // Show default value if any + if defaultVal, hasDefault := actionConfig.Defaults[paramName]; hasDefault { + fmt.Printf(" Default: %v\n", defaultVal) + } + + // Show example value + example := generateExampleValue(def, paramName) + fmt.Printf(" Example: %v\n", example) + + fmt.Println() + } + } + } } + + fmt.Printf("\nπŸ’‘ Use 'darn action schema %s' to see full JSON schema\n", actionConfig.Name) + fmt.Printf("πŸ’‘ Use 'darn action example %s' to see working examples\n", actionConfig.Name) + fmt.Printf("πŸ’‘ Use 'darn action validate %s --parameters file.json' to validate parameters\n", actionConfig.Name) + } else { + fmt.Println("\nParameters: No parameter schema defined") } return nil @@ -409,3 +462,679 @@ func loadParams(filePath string) (map[string]interface{}, error) { } return params, nil } + +// newActionAddCmd creates an 'add' subcommand for creating new actions +func newActionAddCmd() *cobra.Command { + var actionType string + var description string + var command string + var args []string + var templatePath string + var targetPath string + var createDirs bool + var interactiveFlag bool + + addCmd := &cobra.Command{ + Use: "add [action-name]", + Short: "Create a new action", + Long: `Create a new action in the current library. +This command helps you create custom CLI or file actions with proper validation. + +Examples: + # Create a CLI action interactively + darn action add my-security-check --interactive + + # Create a CLI action with flags + darn action add git-check --type cli --command git --args status,--porcelain + + # Create a file action + darn action add add-readme --type file --template readme.md --target README.md`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + actionName := args[0] + + // Load configuration to get library path + cfg, err := config.LoadConfig("", "") + if err != nil { + return fmt.Errorf("error loading configuration: %w", err) + } + + // Use interactive mode if requested + if interactiveFlag { + return createActionInteractive(actionName, cfg) + } + + // Validate required flags + if actionType == "" { + return fmt.Errorf("action type is required (use --type cli or --type file)") + } + + if actionType != "cli" && actionType != "file" { + return fmt.Errorf("action type must be 'cli' or 'file'") + } + + if actionType == "cli" && command == "" { + return fmt.Errorf("command is required for CLI actions (use --command)") + } + + if actionType == "file" && templatePath == "" { + return fmt.Errorf("template path is required for file actions (use --template)") + } + + if actionType == "file" && targetPath == "" { + return fmt.Errorf("target path is required for file actions (use --target)") + } + + // Create the action + return createAction(actionName, actionType, description, command, args, templatePath, targetPath, createDirs, cfg) + }, + } + + // Add flags + addCmd.Flags().StringVarP(&actionType, "type", "t", "", "Action type (cli or file)") + addCmd.Flags().StringVarP(&description, "description", "d", "", "Action description") + addCmd.Flags().StringVarP(&command, "command", "c", "", "Command to execute (for CLI actions)") + addCmd.Flags().StringSliceVarP(&args, "args", "a", []string{}, "Command arguments (for CLI actions)") + addCmd.Flags().StringVar(&templatePath, "template", "", "Template file path (for file actions)") + addCmd.Flags().StringVar(&targetPath, "target", "", "Target file path (for file actions)") + addCmd.Flags().BoolVar(&createDirs, "create-dirs", true, "Create parent directories for target file") + addCmd.Flags().BoolVarP(&interactiveFlag, "interactive", "i", false, "Use interactive mode") + + return addCmd +} + +// createActionInteractive creates an action using interactive prompts +func createActionInteractive(actionName string, cfg *config.Config) error { + fmt.Printf("Creating action '%s' interactively...\n\n", actionName) + + // Prompt for action type + fmt.Print("Action type (cli/file): ") + var actionType string + fmt.Scanln(&actionType) + + if actionType != "cli" && actionType != "file" { + return fmt.Errorf("action type must be 'cli' or 'file'") + } + + // Prompt for description + fmt.Print("Description: ") + var description string + fmt.Scanln(&description) + + var command string + var args []string + var templatePath string + var targetPath string + var createDirs bool = true + + if actionType == "cli" { + // CLI-specific prompts + fmt.Print("Command: ") + fmt.Scanln(&command) + + fmt.Print("Arguments (space-separated, or press enter for none): ") + var argsStr string + fmt.Scanln(&argsStr) + if argsStr != "" { + args = strings.Fields(argsStr) + } + } else { + // File-specific prompts + fmt.Print("Template path: ") + fmt.Scanln(&templatePath) + + fmt.Print("Target path: ") + fmt.Scanln(&targetPath) + + fmt.Print("Create parent directories? (y/n) [y]: ") + var createDirsStr string + fmt.Scanln(&createDirsStr) + createDirs = createDirsStr != "n" && createDirsStr != "no" + } + + return createAction(actionName, actionType, description, command, args, templatePath, targetPath, createDirs, cfg) +} + +// createAction creates the action file +func createAction(actionName, actionType, description, command string, args []string, templatePath, targetPath string, createDirs bool, cfg *config.Config) error { + // Resolve library path + libraryPath := cfg.LibraryPath + if libraryPath == "" { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("error getting user home directory: %w", err) + } + libraryPath = filepath.Join(homeDir, ".darn", "library") + } + + actionsDir := filepath.Join(libraryPath, cfg.ActionsDir) + if cfg.ActionsDir == "" { + actionsDir = filepath.Join(libraryPath, "actions") + } + + // Ensure actions directory exists + if err := os.MkdirAll(actionsDir, 0755); err != nil { + return fmt.Errorf("error creating actions directory: %w", err) + } + + actionFilePath := filepath.Join(actionsDir, fmt.Sprintf("%s.yaml", actionName)) + + // Check if action already exists + if _, err := os.Stat(actionFilePath); err == nil { + return fmt.Errorf("action '%s' already exists at %s", actionName, actionFilePath) + } + + // Create action content + var actionContent strings.Builder + actionContent.WriteString(fmt.Sprintf("name: \"%s\"\n", actionName)) + if description != "" { + actionContent.WriteString(fmt.Sprintf("description: \"%s\"\n", description)) + } else { + actionContent.WriteString(fmt.Sprintf("description: \"Custom %s action\"\n", actionType)) + } + actionContent.WriteString(fmt.Sprintf("type: \"%s\"\n", actionType)) + + if actionType == "cli" { + actionContent.WriteString(fmt.Sprintf("command: \"%s\"\n", command)) + if len(args) > 0 { + actionContent.WriteString("args:\n") + for _, arg := range args { + actionContent.WriteString(fmt.Sprintf(" - \"%s\"\n", arg)) + } + } else { + // Add comment if no args provided + actionContent.WriteString("# args: []\n") + } + } else if actionType == "file" { + actionContent.WriteString(fmt.Sprintf("template_path: \"%s\"\n", templatePath)) + actionContent.WriteString(fmt.Sprintf("target_path: \"%s\"\n", targetPath)) + actionContent.WriteString(fmt.Sprintf("create_dirs: %t\n", createDirs)) + } + + // Add basic parameter schema + actionContent.WriteString("\nparameters:\n") + actionContent.WriteString(" # Add your parameters here\n") + actionContent.WriteString(" # Example:\n") + actionContent.WriteString(" # - name: \"example_param\"\n") + actionContent.WriteString(" # type: \"string\"\n") + actionContent.WriteString(" # required: true\n") + actionContent.WriteString(" # description: \"Example parameter\"\n") + + // Write action file + if err := os.WriteFile(actionFilePath, []byte(actionContent.String()), 0644); err != nil { + return fmt.Errorf("error writing action file: %w", err) + } + + fmt.Printf("βœ… Action '%s' created successfully at %s\n", actionName, actionFilePath) + + // If it's a file action, also suggest creating the template + if actionType == "file" { + templatesDir := filepath.Join(libraryPath, cfg.TemplatesDir) + if cfg.TemplatesDir == "" { + templatesDir = filepath.Join(libraryPath, "templates") + } + templateFilePath := filepath.Join(templatesDir, templatePath) + + if _, err := os.Stat(templateFilePath); os.IsNotExist(err) { + fmt.Printf("\nπŸ’‘ Don't forget to create the template file at: %s\n", templateFilePath) + fmt.Printf(" You can create a basic template with:\n") + fmt.Printf(" mkdir -p %s\n", filepath.Dir(templateFilePath)) + fmt.Printf(" echo '# Template content here' > %s\n", templateFilePath) + } + } + + fmt.Printf("\nπŸ“ Edit the action file to add parameters and customize behavior\n") + fmt.Printf("πŸ” Use 'darn action info %s' to view the action details\n", actionName) + + return nil +} + +// newActionValidateCmd creates a 'validate' subcommand for validating parameters +func newActionValidateCmd() *cobra.Command { + var paramsFile string + + validateCmd := &cobra.Command{ + Use: "validate [action-name]", + Short: "Validate parameters for an action", + Long: `Validate parameters against an action's schema before execution. +This helps catch parameter errors early without running the action. + +Examples: + # Validate parameters from file + darn action validate add-security-md --parameters params.json + + # Validate with inline JSON + darn action validate add-security-md --parameters '{"project_name": "test"}'`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + actionName := args[0] + + if paramsFile == "" { + return fmt.Errorf("parameters are required (use --parameters)") + } + + // Load configuration + cfg, err := config.LoadConfig("", "") + if err != nil { + return fmt.Errorf("error loading configuration: %w", err) + } + + // Get working directory + workingDir, err := os.Getwd() + if err != nil { + return fmt.Errorf("error getting working directory: %w", err) + } + + // Create resolver + context := action.ActionContext{ + TemplatesDir: filepath.Join(workingDir, cfg.TemplatesDir), + WorkingDir: workingDir, + VerboseMode: false, + } + factory := action.NewFactory(context) + factory.RegisterDefaultTypes() + resolver := NewResolver( + factory, + workingDir, + cfg.UseLocal, + cfg.UseGlobal, + cfg.GlobalFirst, + cfg.ActionsDir, + cfg.LibraryPath, + ) + + // Get action config + actionConfig, err := resolver.GetActionConfig(actionName) + if err != nil { + return fmt.Errorf("error loading action: %w", err) + } + + // Load parameters + var params map[string]interface{} + if strings.HasPrefix(paramsFile, "{") { + // Inline JSON + err = json.Unmarshal([]byte(paramsFile), ¶ms) + if err != nil { + return fmt.Errorf("error parsing JSON parameters: %w", err) + } + } else { + // File path + params, err = loadParams(paramsFile) + if err != nil { + return fmt.Errorf("error loading parameters from file: %w", err) + } + } + + // Validate parameters against schema + if actionConfig.Schema != nil { + // Merge with defaults if any + if actionConfig.Defaults != nil { + params = schema.MergeWithDefaults(params, actionConfig.Defaults) + } + + if err := schema.ValidateParams(actionConfig.Schema, params); err != nil { + fmt.Printf("❌ Parameter validation failed for action '%s':\n", actionName) + fmt.Printf(" %v\n", err) + return err + } + + fmt.Printf("βœ… Parameters are valid for action '%s'\n", actionName) + fmt.Printf("πŸ“Š Validated %d parameter(s)\n", len(params)) + + // Show final parameters that would be used + fmt.Println("\nFinal parameters (including defaults):") + paramsJSON, _ := json.MarshalIndent(params, "", " ") + fmt.Println(string(paramsJSON)) + } else { + fmt.Printf("⚠️ Action '%s' has no parameter schema - validation skipped\n", actionName) + } + + return nil + }, + } + + validateCmd.Flags().StringVarP(¶msFile, "parameters", "p", "", "Parameters file (JSON/YAML) or inline JSON string") + + return validateCmd +} + +// newActionSchemaCmd creates a 'schema' subcommand for outputting JSON schema +func newActionSchemaCmd() *cobra.Command { + var outputFormat string + + schemaCmd := &cobra.Command{ + Use: "schema [action-name]", + Short: "Output JSON schema for action parameters", + Long: `Output the JSON schema that defines valid parameters for an action. +This is useful for understanding required parameters, types, and constraints. + +Examples: + # Output JSON schema + darn action schema add-security-md + + # Output as YAML + darn action schema add-security-md --format yaml`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + actionName := args[0] + + // Load configuration + cfg, err := config.LoadConfig("", "") + if err != nil { + return fmt.Errorf("error loading configuration: %w", err) + } + + // Get working directory + workingDir, err := os.Getwd() + if err != nil { + return fmt.Errorf("error getting working directory: %w", err) + } + + // Create resolver + context := action.ActionContext{ + TemplatesDir: filepath.Join(workingDir, cfg.TemplatesDir), + WorkingDir: workingDir, + VerboseMode: false, + } + factory := action.NewFactory(context) + factory.RegisterDefaultTypes() + resolver := NewResolver( + factory, + workingDir, + cfg.UseLocal, + cfg.UseGlobal, + cfg.GlobalFirst, + cfg.ActionsDir, + cfg.LibraryPath, + ) + + // Get action config + actionConfig, err := resolver.GetActionConfig(actionName) + if err != nil { + return fmt.Errorf("error loading action: %w", err) + } + + if actionConfig.Schema == nil { + fmt.Printf("Action '%s' has no parameter schema defined\n", actionName) + return nil + } + + // Output schema in requested format + switch outputFormat { + case "yaml", "yml": + // Convert to YAML format (simplified representation) + fmt.Printf("# Parameter schema for action: %s\n", actionName) + fmt.Printf("# Description: %s\n\n", actionConfig.Description) + + schemaMap := actionConfig.Schema + if props, exists := schemaMap["properties"]; exists { + if properties, ok := props.(map[string]interface{}); ok { + fmt.Println("parameters:") + for paramName, paramDef := range properties { + if def, ok := paramDef.(map[string]interface{}); ok { + fmt.Printf(" %s:\n", paramName) + if pType, exists := def["type"]; exists { + fmt.Printf(" type: %v\n", pType) + } + if desc, exists := def["description"]; exists { + fmt.Printf(" description: \"%v\"\n", desc) + } + if required, exists := schemaMap["required"]; exists { + if reqArray, ok := required.([]interface{}); ok { + isRequired := false + for _, req := range reqArray { + if req == paramName { + isRequired = true + break + } + } + fmt.Printf(" required: %t\n", isRequired) + } + } + } + } + } + } + default: + // JSON format + schemaJSON, err := json.MarshalIndent(actionConfig.Schema, "", " ") + if err != nil { + return fmt.Errorf("error marshaling schema: %w", err) + } + fmt.Println(string(schemaJSON)) + } + + return nil + }, + } + + schemaCmd.Flags().StringVarP(&outputFormat, "format", "f", "json", "Output format (json or yaml)") + + return schemaCmd +} + +// newActionExampleCmd creates an 'example' subcommand for showing working examples +func newActionExampleCmd() *cobra.Command { + var outputFormat string + + exampleCmd := &cobra.Command{ + Use: "example [action-name]", + Short: "Show working parameter examples for an action", + Long: `Show working parameter examples that demonstrate how to use an action. +Examples include both minimal required parameters and full examples with optional parameters. + +Examples: + # Show parameter examples + darn action example add-security-md + + # Output as YAML + darn action example add-security-md --format yaml`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + actionName := args[0] + + // Load configuration + cfg, err := config.LoadConfig("", "") + if err != nil { + return fmt.Errorf("error loading configuration: %w", err) + } + + // Get working directory + workingDir, err := os.Getwd() + if err != nil { + return fmt.Errorf("error getting working directory: %w", err) + } + + // Create resolver + context := action.ActionContext{ + TemplatesDir: filepath.Join(workingDir, cfg.TemplatesDir), + WorkingDir: workingDir, + VerboseMode: false, + } + factory := action.NewFactory(context) + factory.RegisterDefaultTypes() + resolver := NewResolver( + factory, + workingDir, + cfg.UseLocal, + cfg.UseGlobal, + cfg.GlobalFirst, + cfg.ActionsDir, + cfg.LibraryPath, + ) + + // Get action config + actionConfig, err := resolver.GetActionConfig(actionName) + if err != nil { + return fmt.Errorf("error loading action: %w", err) + } + + fmt.Printf("# Parameter Examples for Action: %s\n", actionName) + fmt.Printf("# Description: %s\n", actionConfig.Description) + fmt.Printf("# Type: %s\n\n", actionConfig.Type) + + // Generate examples from schema + examples := generateParameterExamples(*actionConfig) + + if len(examples) == 0 { + fmt.Println("No parameter examples available for this action.") + if actionConfig.Schema == nil { + fmt.Println("Reason: Action has no parameter schema defined.") + } + return nil + } + + // Output examples + for i, example := range examples { + fmt.Printf("## Example %d: %s\n\n", i+1, example.Description) + + switch outputFormat { + case "yaml", "yml": + fmt.Println("```yaml") + yamlData, _ := json.Marshal(example.Parameters) + fmt.Println(string(yamlData)) // Simple JSON representation + fmt.Println("```") + default: + fmt.Println("```json") + exampleJSON, _ := json.MarshalIndent(example.Parameters, "", " ") + fmt.Println(string(exampleJSON)) + fmt.Println("```") + } + + fmt.Printf("\n**Usage:**\n") + fmt.Printf("```bash\n") + if outputFormat == "yaml" { + fmt.Printf("darn action run %s params.yaml\n", actionName) + } else { + fmt.Printf("darn action run %s params.json\n", actionName) + } + fmt.Printf("```\n\n") + } + + return nil + }, + } + + exampleCmd.Flags().StringVarP(&outputFormat, "format", "f", "json", "Output format (json or yaml)") + + return exampleCmd +} + +// ParameterExample represents a working example of parameters +type ParameterExample struct { + Description string `json:"description"` + Parameters map[string]interface{} `json:"parameters"` +} + +// generateParameterExamples creates working examples from action schema +func generateParameterExamples(actionConfig action.Config) []ParameterExample { + var examples []ParameterExample + + if actionConfig.Schema == nil { + return examples + } + + schemaMap := actionConfig.Schema + if schemaMap == nil { + return examples + } + + props, exists := schemaMap["properties"] + if !exists { + return examples + } + + properties, ok := props.(map[string]interface{}) + if !ok { + return examples + } + + // Get required fields + requiredFields := make(map[string]bool) + if required, exists := schemaMap["required"]; exists { + if reqArray, ok := required.([]interface{}); ok { + for _, req := range reqArray { + if reqStr, ok := req.(string); ok { + requiredFields[reqStr] = true + } + } + } + } + + // Generate minimal example (only required fields) + minimalParams := make(map[string]interface{}) + for paramName, paramDef := range properties { + if requiredFields[paramName] { + if def, ok := paramDef.(map[string]interface{}); ok { + example := generateExampleValue(def, paramName) + minimalParams[paramName] = example + } + } + } + + if len(minimalParams) > 0 { + examples = append(examples, ParameterExample{ + Description: "Minimal (required parameters only)", + Parameters: minimalParams, + }) + } + + // Generate full example (all parameters) + fullParams := make(map[string]interface{}) + for paramName, paramDef := range properties { + if def, ok := paramDef.(map[string]interface{}); ok { + // Use default if available, otherwise generate example + if defaultVal, hasDefault := actionConfig.Defaults[paramName]; hasDefault { + fullParams[paramName] = defaultVal + } else { + example := generateExampleValue(def, paramName) + fullParams[paramName] = example + } + } + } + + if len(fullParams) > len(minimalParams) { + examples = append(examples, ParameterExample{ + Description: "Full (all parameters with examples)", + Parameters: fullParams, + }) + } + + return examples +} + +// generateExampleValue creates example values based on parameter schema +func generateExampleValue(paramDef map[string]interface{}, paramName string) interface{} { + paramType, hasType := paramDef["type"] + if !hasType { + return "example_value" + } + + switch paramType { + case "string": + // Generate context-aware examples + lowerName := strings.ToLower(paramName) + if strings.Contains(lowerName, "email") { + return "security@example.com" + } else if strings.Contains(lowerName, "name") { + return "my-project" + } else if strings.Contains(lowerName, "url") { + return "https://example.com" + } else if strings.Contains(lowerName, "path") { + return "/path/to/file" + } else if strings.Contains(lowerName, "dir") { + return "./directory" + } + return "example_string" + case "number", "integer": + return 42 + case "boolean": + return true + case "array": + return []string{"item1", "item2"} + case "object": + return map[string]interface{}{"key": "value"} + default: + return "example_value" + } +} diff --git a/cmd/darn/cmd/library/diagnose.go b/cmd/darn/cmd/library/diagnose.go new file mode 100644 index 0000000..3d870af --- /dev/null +++ b/cmd/darn/cmd/library/diagnose.go @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: Apache-2.0 + +package library + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/kusari-oss/darn/internal/core/config" + "github.com/spf13/cobra" +) + +var diagnoseCmd = &cobra.Command{ + Use: "diagnose", + Short: "Diagnose library configuration issues", + Long: `Diagnose library configuration and path resolution issues. + +This command provides detailed information about: +- Library path resolution order and results +- Configuration sources and their values +- Directory existence and accessibility +- Common configuration problems + +Use this command when you're experiencing issues with library paths, +missing actions, or environment-specific problems.`, + RunE: runDiagnoseCommand, +} + +var ( + jsonOutput bool + verbose bool +) + +func init() { + diagnoseCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output in JSON format") + diagnoseCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output") +} + +func runDiagnoseCommand(cmd *cobra.Command, args []string) error { + // Load configuration + cfg, err := config.LoadConfig("", "") + if err != nil { + return fmt.Errorf("failed to load configuration: %w", err) + } + + // Enable verbose logging if requested + if verbose { + cfg.SetVerboseLibraryLogging(true) + } + + // Get diagnostic information + diagnostics := cfg.GetLibraryDiagnostics() + + // Try to resolve library path + libraryInfo, libraryErr := cfg.GetLibraryInfo() + if libraryInfo != nil { + diagnostics["library_resolution"] = map[string]interface{}{ + "path": libraryInfo.Path, + "source": libraryInfo.Source, + "valid": libraryInfo.Valid, + "exists": libraryInfo.Exists, + "errors": libraryInfo.Errors, + "subdirectories": map[string]string{ + "actions": libraryInfo.ActionsDir, + "templates": libraryInfo.TemplatesDir, + "configs": libraryInfo.ConfigsDir, + "mappings": libraryInfo.MappingsDir, + }, + } + } + if libraryErr != nil { + diagnostics["library_error"] = libraryErr.Error() + } + + // Test command availability for common commands + commonCommands := []string{"sh", "bash", "cmd", "echo", "mkdir", "cp", "mv"} + commandTests := make(map[string]interface{}) + for _, cmd := range commonCommands { + if err := cfg.ValidateCommand(cmd); err != nil { + commandTests[cmd] = map[string]interface{}{ + "available": false, + "error": err.Error(), + } + } else { + commandTests[cmd] = map[string]interface{}{ + "available": true, + } + } + } + diagnostics["commands"] = commandTests + + // Add current working directory + if cwd, err := os.Getwd(); err == nil { + diagnostics["current_directory"] = cwd + } + + // Add environment variables + diagnostics["environment"] = map[string]string{ + "HOME": os.Getenv("HOME"), + "DARN_HOME": os.Getenv("DARN_HOME"), + "PATH": os.Getenv("PATH"), + } + + // Output results + if jsonOutput { + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent("", " ") + return encoder.Encode(diagnostics) + } + + // Human-readable output + fmt.Println("=== Darn Library Diagnostics ===") + fmt.Println() + + // Library resolution status + fmt.Println("Library Resolution:") + if libraryInfo != nil { + if libraryInfo.Valid { + fmt.Printf(" βœ“ Library found and valid: %s\n", libraryInfo.Path) + fmt.Printf(" Source: %s\n", libraryInfo.Source) + } else { + fmt.Printf(" βœ— Library invalid: %s\n", libraryInfo.Path) + fmt.Printf(" Source: %s\n", libraryInfo.Source) + for _, err := range libraryInfo.Errors { + fmt.Printf(" Error: %s\n", err) + } + } + } else { + fmt.Printf(" βœ— No library path resolved\n") + if libraryErr != nil { + fmt.Printf(" Error: %s\n", libraryErr.Error()) + } + } + fmt.Println() + + // Configuration sources + fmt.Println("Configuration Sources:") + if cmdLineLib := diagnostics["cmdline_library_path"]; cmdLineLib != nil && cmdLineLib != "" { + fmt.Printf(" Command line: %s\n", cmdLineLib) + } else { + fmt.Printf(" Command line: (not set)\n") + } + + if darnHome := diagnostics["darn_home"]; darnHome != nil && darnHome != "" { + fmt.Printf(" DARN_HOME: %s\n", darnHome) + } else { + fmt.Printf(" DARN_HOME: (not set)\n") + } + + if globalLib := diagnostics["global_library_path"]; globalLib != nil && globalLib != "" { + fmt.Printf(" Global config: %s\n", globalLib) + } else { + fmt.Printf(" Global config: (not set)\n") + } + + if userHome := diagnostics["user_home"]; userHome != nil { + fmt.Printf(" User home: %s\n", userHome) + } + fmt.Println() + + // Command availability + fmt.Println("Command Availability:") + for cmd, result := range commandTests { + resultMap := result.(map[string]interface{}) + if resultMap["available"].(bool) { + fmt.Printf(" βœ“ %s\n", cmd) + } else { + fmt.Printf(" βœ— %s - %s\n", cmd, resultMap["error"]) + } + } + fmt.Println() + + // Recommendations + fmt.Println("Recommendations:") + if libraryInfo == nil || !libraryInfo.Valid { + fmt.Println(" 1. Initialize a library with: darn library init") + fmt.Println(" 2. Or set a custom library path with: darn library set-global ") + } + + // Check for missing common shell commands + missingCommands := []string{} + for cmd, result := range commandTests { + resultMap := result.(map[string]interface{}) + if !resultMap["available"].(bool) { + missingCommands = append(missingCommands, cmd) + } + } + if len(missingCommands) > 0 { + fmt.Printf(" 3. Install missing commands: %v\n", missingCommands) + } + + return nil +} \ No newline at end of file diff --git a/cmd/darn/cmd/library/library.go b/cmd/darn/cmd/library/library.go index 602478b..f03a51d 100644 --- a/cmd/darn/cmd/library/library.go +++ b/cmd/darn/cmd/library/library.go @@ -33,6 +33,12 @@ func NewLibraryCommand() *cobra.Command { setGlobalCmd := newSetGlobalCommand() libraryCmd.AddCommand(setGlobalCmd) + // Add diagnose subcommand + libraryCmd.AddCommand(diagnoseCmd) + + // Add sync subcommand + libraryCmd.AddCommand(syncCmd) + return libraryCmd } diff --git a/cmd/darn/cmd/library/sync.go b/cmd/darn/cmd/library/sync.go new file mode 100644 index 0000000..bbf09ce --- /dev/null +++ b/cmd/darn/cmd/library/sync.go @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: Apache-2.0 + +package library + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/kusari-oss/darn/internal/core/config" + "github.com/kusari-oss/darn/internal/defaults" + "github.com/spf13/cobra" +) + +var syncCmd = &cobra.Command{ + Use: "sync", + Aliases: []string{"refresh"}, + Short: "Sync the global library with latest embedded defaults", + Long: `Sync the global library with the latest embedded defaults from the darn binary. + +This command updates your global library (typically ~/.darn/library) with the latest +action definitions, templates, configs, and mappings that are embedded in the darn binary. + +This is useful when: +- You've updated darn and want the latest default actions/templates +- Your library is missing some default components +- You want to restore default functionality + +Examples: + darn library sync # Sync the currently configured global library + darn library sync --library-path /custom/path # Sync a specific library + darn library sync --dry-run # Show what would be synced without making changes + darn library sync --force # Overwrite existing files even if they're newer`, + RunE: runSyncCommand, +} + +var ( + syncLibraryPath string + syncDryRun bool + syncForce bool + syncVerbose bool + syncLocalOnly bool +) + +func init() { + syncCmd.Flags().StringVar(&syncLibraryPath, "library-path", "", "Path to library to sync (defaults to global library)") + syncCmd.Flags().BoolVar(&syncDryRun, "dry-run", false, "Show what would be synced without making changes") + syncCmd.Flags().BoolVar(&syncForce, "force", false, "Overwrite existing files even if they're newer") + syncCmd.Flags().BoolVarP(&syncVerbose, "verbose", "v", false, "Verbose output") + syncCmd.Flags().BoolVar(&syncLocalOnly, "local-only", false, "Use only embedded defaults, don't attempt remote fetch") +} + +func runSyncCommand(cmd *cobra.Command, args []string) error { + // Load configuration + cfg, err := config.LoadConfig("", "") + if err != nil { + return fmt.Errorf("failed to load configuration: %w", err) + } + + // Determine target library path + var targetLibraryPath string + if syncLibraryPath != "" { + targetLibraryPath = config.ExpandPathWithTilde(syncLibraryPath) + if syncVerbose { + fmt.Printf("Using library path from --library-path flag: %s\n", targetLibraryPath) + } + } else { + // Use global library + if cfg.LibraryManager == nil { + cfg.LibraryManager = cfg.LibraryManager + } + + libraryInfo, err := cfg.GetLibraryInfo() + if err != nil { + return fmt.Errorf("failed to resolve global library path: %w", err) + } + + targetLibraryPath = libraryInfo.Path + if syncVerbose { + fmt.Printf("Using global library path: %s (source: %s)\n", targetLibraryPath, libraryInfo.Source) + } + } + + // Ensure target library exists + absTargetPath, err := filepath.Abs(targetLibraryPath) + if err != nil { + return fmt.Errorf("failed to resolve absolute path for %s: %w", targetLibraryPath, err) + } + + if syncVerbose { + fmt.Printf("Target library path: %s\n", absTargetPath) + } + + // Create defaults manager + defaultsConfig := defaults.DefaultsConfig{ + DefaultsURL: "https://raw.githubusercontent.com/kusari-oss/darn-defaults/main", + UseRemote: !syncLocalOnly, + Timeout: 10, + } + manager := defaults.NewManager(defaultsConfig) + + // Define subdirectory paths + subdirs := map[string]string{ + "actions": filepath.Join(absTargetPath, "actions"), + "templates": filepath.Join(absTargetPath, "templates"), + "configs": filepath.Join(absTargetPath, "configs"), + "mappings": filepath.Join(absTargetPath, "mappings"), + } + + if syncDryRun { + fmt.Printf("πŸ” DRY RUN: Would sync to library at: %s\n", absTargetPath) + + // Check what exists + for name, path := range subdirs { + if _, err := os.Stat(path); os.IsNotExist(err) { + fmt.Printf(" Would create: %s/\n", name) + } else { + fmt.Printf(" Would update: %s/\n", name) + } + } + + fmt.Printf("\nTo perform the actual sync, run without --dry-run\n") + return nil + } + + fmt.Printf("πŸ”„ Syncing library at: %s\n", absTargetPath) + + // Create target directories + for name, path := range subdirs { + if err := os.MkdirAll(path, 0755); err != nil { + return fmt.Errorf("failed to create %s directory: %w", name, err) + } + } + + // Perform the sync using the defaults manager + usedRemote, err := manager.CopyDefaults( + subdirs["templates"], + subdirs["actions"], + subdirs["configs"], + subdirs["mappings"], + !syncLocalOnly, + ) + if err != nil { + return fmt.Errorf("failed to sync defaults: %w", err) + } + + // Success message + fmt.Printf("βœ… Library sync complete!\n") + fmt.Printf(" πŸ“ Actions: %s\n", subdirs["actions"]) + fmt.Printf(" πŸ“„ Templates: %s\n", subdirs["templates"]) + fmt.Printf(" βš™οΈ Configs: %s\n", subdirs["configs"]) + fmt.Printf(" πŸ—ΊοΈ Mappings: %s\n", subdirs["mappings"]) + + if syncLocalOnly { + fmt.Printf(" πŸ“¦ Used embedded defaults (local-only mode)\n") + } else { + fmt.Printf(" πŸ“¦ Used %s defaults\n", map[bool]string{true: "remote", false: "embedded"}[usedRemote]) + } + + // Show some available actions + fmt.Printf("\nπŸ“‹ Available actions:\n") + if actions, err := cfg.LibraryManager.ListAvailableActions(absTargetPath); err == nil { + count := 0 + for _, action := range actions { + if count >= 5 { + break + } + fmt.Printf(" - %s\n", action) + count++ + } + if len(actions) > 5 { + fmt.Printf(" ... and %d more\n", len(actions)-5) + } + } + + return nil +} \ No newline at end of file diff --git a/cmd/darnit/cmd/mapping/mapping.go b/cmd/darnit/cmd/mapping/mapping.go index 7be09e8..5fada83 100644 --- a/cmd/darnit/cmd/mapping/mapping.go +++ b/cmd/darnit/cmd/mapping/mapping.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/kusari-oss/darn/internal/core/config" "github.com/kusari-oss/darn/internal/darnit/plan" @@ -22,6 +23,7 @@ func GetMappingCmd() *cobra.Command { mappingCmd.AddCommand(getListCmd()) mappingCmd.AddCommand(getValidateCmd()) + mappingCmd.AddCommand(getAddCmd()) return mappingCmd } @@ -109,3 +111,210 @@ func getValidateCmd() *cobra.Command { return validateCmd } + +func getAddCmd() *cobra.Command { + var interactive bool + var condition string + var action string + var reason string + var mappingID string + + addCmd := &cobra.Command{ + Use: "add [mapping-file]", + Short: "Create a new mapping file or add rules to existing mapping", + Long: `Create a new mapping file or add mapping rules to an existing file. +This command helps you create custom mapping rules that link security findings to remediation actions. + +Examples: + # Create a new mapping file interactively + darnit mapping add security-rules.yaml --interactive + + # Add a rule with flags + darnit mapping add security-rules.yaml --id missing-policy --condition "security_policy == 'missing'" --action add-security-md --reason "Add security policy"`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + mappingFile := args[0] + + // Load configuration to get mappings directory + cfg, err := config.LoadConfig("", "") + if err != nil { + return fmt.Errorf("error loading configuration: %w", err) + } + + if interactive { + return createMappingInteractive(mappingFile, cfg) + } + + // Validate required flags for non-interactive mode + if mappingID == "" { + return fmt.Errorf("mapping ID is required (use --id)") + } + if condition == "" { + return fmt.Errorf("condition is required (use --condition)") + } + if action == "" { + return fmt.Errorf("action is required (use --action)") + } + if reason == "" { + return fmt.Errorf("reason is required (use --reason)") + } + + return createMapping(mappingFile, mappingID, condition, action, reason, cfg) + }, + } + + // Add flags + addCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Use interactive mode") + addCmd.Flags().StringVar(&mappingID, "id", "", "Mapping rule ID") + addCmd.Flags().StringVarP(&condition, "condition", "c", "", "CEL condition expression") + addCmd.Flags().StringVarP(&action, "action", "a", "", "Action to execute") + addCmd.Flags().StringVarP(&reason, "reason", "r", "", "Reason for this mapping") + + return addCmd +} + +func createMappingInteractive(mappingFile string, cfg *config.Config) error { + fmt.Printf("Creating mapping '%s' interactively...\n\n", mappingFile) + + var rules []plan.MappingRule + + for { + fmt.Println("Add a new mapping rule:") + + // Prompt for mapping ID + fmt.Print("Rule ID: ") + var mappingID string + fmt.Scanln(&mappingID) + + // Prompt for condition + fmt.Print("Condition (CEL expression): ") + var condition string + fmt.Scanln(&condition) + + // Prompt for action + fmt.Print("Action name: ") + var action string + fmt.Scanln(&action) + + // Prompt for reason + fmt.Print("Reason: ") + var reason string + fmt.Scanln(&reason) + + // Create the rule + rule := plan.MappingRule{ + ID: mappingID, + Condition: condition, + Action: action, + Reason: reason, + } + + rules = append(rules, rule) + + // Ask if user wants to add more rules + fmt.Print("Add another rule? (y/n): ") + var continueStr string + fmt.Scanln(&continueStr) + if continueStr != "y" && continueStr != "yes" { + break + } + fmt.Println() + } + + return writeMappingFile(mappingFile, rules, cfg) +} + +func createMapping(mappingFile, mappingID, condition, action, reason string, cfg *config.Config) error { + rule := plan.MappingRule{ + ID: mappingID, + Condition: condition, + Action: action, + Reason: reason, + } + + return writeMappingFile(mappingFile, []plan.MappingRule{rule}, cfg) +} + +func writeMappingFile(mappingFile string, rules []plan.MappingRule, cfg *config.Config) error { + // Resolve mappings directory + workingDir, err := os.Getwd() + if err != nil { + return fmt.Errorf("error getting working directory: %w", err) + } + + mappingsDir := filepath.Join(workingDir, cfg.MappingsDir) + if cfg.MappingsDir == "" { + mappingsDir = filepath.Join(workingDir, "mappings") + } + + // Ensure mappings directory exists + if err := os.MkdirAll(mappingsDir, 0755); err != nil { + return fmt.Errorf("error creating mappings directory: %w", err) + } + + // Determine full file path + var mappingFilePath string + if filepath.IsAbs(mappingFile) { + mappingFilePath = mappingFile + } else { + mappingFilePath = filepath.Join(mappingsDir, mappingFile) + } + + // Ensure .yaml extension + if !strings.HasSuffix(mappingFilePath, ".yaml") && !strings.HasSuffix(mappingFilePath, ".yml") { + mappingFilePath += ".yaml" + } + + // Check if file exists and load existing rules + var existingRules []plan.MappingRule + if _, err := os.Stat(mappingFilePath); err == nil { + // File exists, load existing rules + existingMapping, err := plan.LoadMappingConfig(mappingFilePath) + if err != nil { + return fmt.Errorf("error loading existing mapping file: %w", err) + } + existingRules = existingMapping.Mappings + } + + // Combine existing and new rules + allRules := append(existingRules, rules...) + + // Create mapping content + var content strings.Builder + content.WriteString("# Mapping rules for security remediation\n") + content.WriteString("# This file defines conditions that trigger specific remediation actions\n\n") + content.WriteString("mappings:\n") + + for _, rule := range allRules { + content.WriteString(fmt.Sprintf(" - id: \"%s\"\n", rule.ID)) + content.WriteString(fmt.Sprintf(" condition: \"%s\"\n", rule.Condition)) + content.WriteString(fmt.Sprintf(" action: \"%s\"\n", rule.Action)) + content.WriteString(fmt.Sprintf(" reason: \"%s\"\n", rule.Reason)) + + // Add parameters section with example + content.WriteString(" parameters:\n") + content.WriteString(" # Add your parameters here\n") + content.WriteString(" # Example:\n") + content.WriteString(" # project_name: \"{{.project_name}}\"\n") + content.WriteString(" # security_email: \"{{.security_email}}\"\n") + content.WriteString("\n") + } + + // Write mapping file + if err := os.WriteFile(mappingFilePath, []byte(content.String()), 0644); err != nil { + return fmt.Errorf("error writing mapping file: %w", err) + } + + if len(existingRules) > 0 { + fmt.Printf("βœ… Added %d new rule(s) to existing mapping file: %s\n", len(rules), mappingFilePath) + fmt.Printf("πŸ“Š File now contains %d total rule(s)\n", len(allRules)) + } else { + fmt.Printf("βœ… Mapping file '%s' created successfully at %s\n", filepath.Base(mappingFilePath), mappingFilePath) + fmt.Printf("πŸ“Š File contains %d rule(s)\n", len(rules)) + } + + fmt.Printf("\nπŸ“ Edit the mapping file to add parameters and customize behavior\n") + fmt.Printf("πŸ” Use 'darnit mapping validate %s' to validate the mapping\n", mappingFilePath) + + return nil +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..c3ac936 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,244 @@ +# Darn Architecture Overview + +This document provides a high-level overview of how darn components work together. + +## System Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Security β”‚ β”‚ Report β”‚ β”‚ Parameters β”‚ +β”‚ Scanner │───▢│ findings.json │◀───│ params.json β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ darnit β”‚ + β”‚ plan generate β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Mappings │◀── Library System + β”‚ Rules Engine β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ Actions β”‚ + β”‚ β”‚ Templates β”‚ + β–Ό β”‚ Configs β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Mappings β”‚ + β”‚ Remediation β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ Plan (JSON) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ darnit β”‚ + β”‚ plan execute β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Remediation β”‚ + β”‚ Actions β”‚ + β”‚ Executed β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Component Breakdown + +### 1. Input Layer + +**Security Scanner** +- External tools (e.g., GitHub Security Scanner, Snyk, etc.) +- Generates structured findings in JSON format +- Examples: Missing security policies, vulnerable dependencies, weak configurations + +**Parameters** +- Project-specific variables (project name, maintainer email, etc.) +- Environment settings (production, staging, development) +- Policy preferences (security levels, compliance requirements) + +### 2. Processing Layer + +**darnit plan generate** +- Takes security findings + parameters as input +- Uses mapping rules to determine appropriate remediation actions +- Generates executable remediation plan +- Handles dependency resolution between actions + +**Mapping Rules Engine** +- CEL (Common Expression Language) for condition evaluation +- Maps security findings to specific remediation actions +- Supports complex conditions and nested mappings +- Extensible rule definitions + +### 3. Library System + +**Actions** +- CLI commands (execute external tools) +- File operations (create/modify files from templates) +- Parameterized and reusable + +**Templates** +- Reusable content patterns +- Go template syntax with variables +- Organized by category (security policies, licenses, etc.) + +**Mappings** +- Condition β†’ Action mappings +- Hierarchical rule definitions +- Reference other mappings for complex scenarios + +**Configs** +- Reusable configuration presets +- Environment-specific settings +- Policy templates + +### 4. Execution Layer + +**darnit plan execute** +- Executes actions defined in remediation plan +- Handles action dependencies and ordering +- Provides execution feedback and error handling +- Supports dry-run mode for testing + +### 5. Management Layer + +**darn CLI** +- Library management (init, sync, diagnose) +- Action inspection and testing +- Configuration management +- Cross-platform support + +## Data Flow + +### 1. Detection Phase +``` +Scanner β†’ findings.json + params.json +``` + +### 2. Planning Phase +``` +findings.json + params.json + mappings β†’ remediation-plan.json +``` + +### 3. Execution Phase +``` +remediation-plan.json + library β†’ executed actions +``` + +## Key Design Principles + +### 1. **Separation of Concerns** +- **Detection**: External scanners focus on finding issues +- **Planning**: darnit focuses on determining what to do +- **Execution**: Actions focus on how to fix issues +- **Management**: darn CLI focuses on library organization + +### 2. **Extensibility** +- Plugin architecture for actions +- Template-based content generation +- Rule-based mapping system +- Library-based organization + +### 3. **Safety** +- Dry-run capabilities for testing +- Input validation and sanitization +- No shell injection vulnerabilities +- Explicit dependency management + +### 4. **Flexibility** +- Multiple library support (global, project-specific, testing) +- Environment variable overrides +- Configurable precedence rules +- Cross-platform compatibility + +## Security Model + +### Input Validation +- JSON schema validation for findings and parameters +- Template variable sanitization +- Path validation for file operations +- Command existence verification + +### Execution Safety +- No shell command injection +- Controlled command execution via exec.Command +- File system boundary enforcement +- Permission validation + +### Library Isolation +- Separate library instances for different contexts +- Environment-based overrides for testing +- Validation of library structure and contents +- Secure defaults for new libraries + +## Extension Points + +### Custom Actions +```yaml +# Custom action definition +name: "custom-security-check" +type: "cli" +command: "my-tool" +args: ["--flag", "{{.value}}"] +parameters: + - name: "value" + type: "string" + required: true +``` + +### Custom Templates +```go +// Template with custom logic +{{if .environment == "production"}} + production-specific content +{{else}} + development content +{{end}} +``` + +### Custom Mappings +```yaml +# Custom mapping rule +mappings: + - id: "custom-check" + condition: "custom_field == 'trigger_value'" + action: "custom-action" + parameters: + value: "{{.custom_parameter}}" +``` + +### Integration APIs +```go +// Programmatic usage +config := darnit.GenerateOptions{ + MappingsDir: "/custom/mappings", + ExtraParams: map[string]any{ + "environment": "production", + }, +} + +plan, err := darnit.GenerateRemediationPlan(report, mappingFile, config) +``` + +## Scalability Considerations + +### Performance +- Parallel action execution where possible +- Efficient CEL expression evaluation +- Template compilation and caching +- Library path resolution caching + +### Large-Scale Deployment +- Centralized library management +- Policy as code through mappings +- Batch processing capabilities +- Integration with CI/CD systems + +### Monitoring +- Execution logging and metrics +- Action success/failure tracking +- Performance monitoring +- Library usage analytics + +This architecture enables darn to provide a flexible, secure, and scalable solution for automated security remediation while maintaining clear separation of concerns and extensibility. \ No newline at end of file diff --git a/docs/LIBRARY_MANAGEMENT.md b/docs/LIBRARY_MANAGEMENT.md new file mode 100644 index 0000000..4ab6975 --- /dev/null +++ b/docs/LIBRARY_MANAGEMENT.md @@ -0,0 +1,182 @@ +# Improved Library Management System + +The darn library system has been enhanced with robust path resolution, validation, and error reporting to provide a more reliable and debuggable experience. + +## Key Improvements + +### 1. Clear Path Resolution Precedence + +The library path is resolved using a clear hierarchy (highest to lowest priority): + +1. **Command line flag** (`--library-path`) +2. **DARN_HOME environment variable** (for testing) +3. **Global config file setting** (`~/.darn/config.yaml`) +4. **Default global library** (`~/.darn/library`) + +### 2. Robust Validation + +- **Library structure validation**: Checks for required subdirectories (`actions/`, `templates/`, `configs/`, `mappings/`) +- **Path accessibility**: Verifies directories exist and are readable +- **CLI command validation**: Validates that CLI commands exist in PATH (for `cli` action type) +- **Cross-platform support**: Handles Windows executable extensions + +### 3. Better Error Reporting + +- **Detailed error messages**: Clear indication of what went wrong and where +- **Verbose logging**: Optional detailed output for debugging +- **Diagnostic command**: Built-in troubleshooting tool + +## Usage + +### Diagnosing Issues + +When experiencing library or shell command issues, use the diagnostic command: + +```bash +# Basic diagnostics +darn library diagnose + +# Verbose output for debugging +darn library diagnose -v + +# JSON output for automation +darn library diagnose --json +``` + +### Library Switching for Testing + +For testing or project-specific libraries, use the `DARN_HOME` environment variable: + +```bash +# Use a temporary library for testing +DARN_HOME=/tmp/test-library darn library diagnose + +# Set up a project-specific library +export DARN_HOME=/path/to/project-library +darn action list +``` + +### Setting Up Libraries + +```bash +# Initialize a new library at default location +darn library init + +# Initialize at custom location +darn library init /path/to/custom/library + +# Set global library path +darn library set-global /path/to/custom/library + +# Sync library with latest embedded defaults +darn library sync + +# Sync with verbose output +darn library sync --verbose + +# Preview what would be synced (dry run) +darn library sync --dry-run +``` + +## Technical Details + +### Library Manager + +The new `library.Manager` provides: + +- **Path resolution** with environment-specific handling +- **Validation** of library structure and shell commands +- **Diagnostics** for troubleshooting +- **Cross-platform** compatibility + +### Configuration Integration + +The enhanced `Config` struct includes: + +- `LibraryManager` for robust path handling +- `ValidateLibrarySetup()` for upfront validation +- `GetLibraryDiagnostics()` for debugging information +- `ValidateShellCommand()` for command validation + +### Error Handling + +Instead of silent failures, the system now: + +- **Validates** library paths at startup +- **Reports** specific error conditions +- **Provides** actionable recommendations +- **Logs** resolution attempts in verbose mode + +## Troubleshooting + +### Common Issues + +1. **Library not found** + ``` + Solution: Run `darn library init` or `darn library set-global ` + ``` + +2. **CLI commands not found** + ``` + Solution: Install missing commands or check PATH environment variable + ``` + +3. **Permission errors** + ``` + Solution: Check directory permissions and user access rights + ``` + +### Environment-Specific Problems + +The diagnostic command identifies: + +- Path resolution issues +- Missing directories +- Permission problems +- Shell command availability +- Environment variable settings + +### Testing Different Configurations + +Use `DARN_HOME` for temporary testing: + +```bash +# Create test library +mkdir -p /tmp/test-lib/{actions,templates,configs,mappings} + +# Test with temporary library +DARN_HOME=/tmp/test-lib darn library diagnose + +# Run operations with test library +DARN_HOME=/tmp/test-lib darn action list +``` + +## CLI-Based Library Management + +The new CLI-based approach provides several commands for managing libraries: + +- **`darn library init`** - Initialize new libraries with standard structure +- **`darn library sync`** - Update library with latest embedded defaults +- **`darn library diagnose`** - Troubleshoot library configuration issues +- **`darn library set-global`** - Configure global library path +- **`darn library update`** - Update library from source directory + +### Benefits over shell scripts: +- βœ… **Cross-platform** - Works on Windows, macOS, Linux +- βœ… **Integrated validation** - Checks paths and permissions +- βœ… **Consistent interface** - Same CLI patterns as other commands +- βœ… **Better error handling** - Clear, actionable error messages +- βœ… **Dry-run support** - Preview changes before applying + +## Migration from Old System + +The improved system is backward compatible, but provides: + +- **Better error messages** when things go wrong +- **Validation** that catches issues early +- **Diagnostics** for troubleshooting problems +- **Consistent behavior** across environments +- **CLI-based management** instead of shell scripts + +Existing configurations will continue to work, but you'll get better feedback when there are issues. + diff --git a/docs/LIBRARY_QUICKSTART.md b/docs/LIBRARY_QUICKSTART.md new file mode 100644 index 0000000..8274e15 --- /dev/null +++ b/docs/LIBRARY_QUICKSTART.md @@ -0,0 +1,365 @@ +# Darn Library Quick Start Guide + +This guide helps you get started with the darn library system quickly. + +## 5-Minute Setup + +### 1. Initialize Your Library + +```bash +# Create library at default location (~/.darn/library) +darn library init + +# Verify it's working +darn library diagnose +``` + +### 2. List Available Actions + +```bash +# See all available actions +darn action list + +# Look for specific actions +darn action list | grep security +``` + +### 3. Use an Action + +```bash +# Show action details +darn action show add-security-md + +# Run an action (this will be implemented in action execution) +# darn action run add-security-md --project-name "My Project" --security-email "security@example.com" +``` + +## Common Tasks + +### Adding Custom Actions + +```bash +# Create a simple CLI action +cat > ~/.darn/library/actions/hello-world.yaml << 'EOF' +name: "hello-world" +description: "Simple greeting action" +type: "cli" +command: "echo" +args: ["Hello {{.name}}!"] + +parameters: + - name: "name" + type: "string" + required: true + description: "Name to greet" +EOF + +# Test it +darn action show hello-world +``` + +### Creating File Actions + +```bash +# Create template +cat > ~/.darn/library/templates/readme.md << 'EOF' +# {{.project_name}} + +{{.description}} + +## Installation + +```bash +npm install {{.package_name}} +``` + +## Usage + +See examples in the documentation. + +## Contributing + +Please read CONTRIBUTING.md for contribution guidelines. +EOF + +# Create action that uses the template +cat > ~/.darn/library/actions/add-readme.yaml << 'EOF' +name: "add-readme" +description: "Add README.md file to project" +type: "file" +template_path: "readme.md" +target_path: "README.md" +create_dirs: false + +parameters: + - name: "project_name" + type: "string" + required: true + - name: "description" + type: "string" + required: true + - name: "package_name" + type: "string" + required: false + default: "{{.project_name}}" +EOF +``` + +### Library Management + +```bash +# Update library with latest defaults +darn library sync + +# Preview what would be updated +darn library sync --dry-run + +# Set custom library location +darn library set-global /path/to/custom/library + +# Troubleshoot issues +darn library diagnose --verbose +``` + +### Working with Different Libraries + +```bash +# Use temporary library for testing +mkdir -p /tmp/test-lib/{actions,templates,configs,mappings} + +# Test with temporary library +DARN_HOME=/tmp/test-lib darn action list + +# Copy action to test library +cp ~/.darn/library/actions/add-security-md.yaml /tmp/test-lib/actions/ + +# Use test library +DARN_HOME=/tmp/test-lib darn action show add-security-md +``` + +## Action Examples + +### 1. Simple CLI Action + +```yaml +# actions/git-init.yaml +name: "git-init" +description: "Initialize git repository" +type: "cli" +command: "git" +args: ["init", "{{.directory}}"] + +parameters: + - name: "directory" + type: "string" + required: false + default: "." +``` + +### 2. File Creation Action + +```yaml +# actions/create-gitignore.yaml +name: "create-gitignore" +description: "Create .gitignore file" +type: "file" +template_path: "gitignore.txt" +target_path: ".gitignore" +create_dirs: false + +parameters: + - name: "language" + type: "string" + required: true + description: "Programming language (node, python, go, etc.)" +``` + +### 3. Action with Outputs + +```yaml +# actions/create-config.yaml +name: "create-config" +description: "Create configuration file" +type: "file" +template_path: "config.json" +target_path: "{{.config_dir}}/{{.config_name}}.json" +create_dirs: true + +parameters: + - name: "config_name" + type: "string" + required: true + - name: "config_dir" + type: "string" + default: "config" + - name: "environment" + type: "string" + default: "development" + +outputs: + config_path: "{{.config_dir}}/{{.config_name}}.json" +``` + +## Template Examples + +### 1. Simple Template + +```markdown + +MIT License + +Copyright (c) {{.year}} {{.author}} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction... +``` + +### 2. Conditional Template + +```yaml +# templates/github-workflow.yml +name: {{.workflow_name}} + +on: + push: + branches: [ {{.main_branch}} ] + pull_request: + branches: [ {{.main_branch}} ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + {{if .run_tests}} + - name: Run Tests + run: {{.test_command}} + {{end}} + + {{if .run_security_scan}} + - name: Security Scan + run: {{.security_command}} + {{end}} +``` + +### 3. Loop Template + +```markdown + +# Team Contacts + +{{range .team_members}} +## {{.name}} +- Role: {{.role}} +- Email: {{.email}} +{{if .slack_handle}}- Slack: {{.slack_handle}}{{end}} + +{{end}} +``` + +## Mapping Examples + +### 1. Basic Mapping + +```yaml +# mappings/basic-security.yaml +mappings: + - id: "add-security-policy" + condition: "security_policy == 'missing'" + action: "add-security-md" + reason: "Repository needs security policy" + parameters: + project_name: "{{.project_name}}" + security_email: "security@company.com" + + - id: "add-license" + condition: "license == 'missing'" + action: "add-license-mit" + reason: "Repository needs license" + parameters: + author: "{{.author}}" + year: "{{.current_year}}" +``` + +### 2. Complex Conditions + +```yaml +# mappings/advanced-security.yaml +mappings: + - id: "enable-mfa" + condition: "mfa_enabled == false && admin_count > 0" + action: "setup-mfa" + reason: "MFA required for repositories with admin users" + + - id: "branch-protection" + condition: "branch_protection == 'none' || (branch_protection == 'basic' && sensitivity == 'high')" + action: "enable-branch-protection" + reason: "Strengthen branch protection for sensitive repository" +``` + +## Troubleshooting + +### Common Issues + +**Library not found:** +```bash +darn library diagnose +# Check the path resolution and create library if needed +darn library init +``` + +**Action not found:** +```bash +darn action list | grep action-name +# If missing, check if it exists in library +ls ~/.darn/library/actions/ +``` + +**Command not found (CLI actions):** +```bash +darn library diagnose +# Check "Command Availability" section +# Install missing commands or update PATH +``` + +**Template not found (File actions):** +```bash +ls ~/.darn/library/templates/ +# Ensure template file exists and path is correct in action definition +``` + +### Debug Mode + +```bash +# Verbose library operations +darn library sync --verbose + +# Detailed diagnostics +darn library diagnose --verbose --json +``` + +### Testing Changes + +```bash +# Always test with dry-run first +darn library sync --dry-run + +# Use test library for experiments +DARN_HOME=/tmp/test-lib darn action show my-test-action +``` + +## Next Steps + +1. **Read the full documentation**: See `LIBRARY_SYSTEM.md` for complete details +2. **Create custom actions**: Start with simple CLI or file actions +3. **Set up mappings**: Define rules for automatic remediation +4. **Integrate with workflows**: Use in CI/CD pipelines or scripts + +## Getting Help + +- **Diagnose issues**: `darn library diagnose` +- **Check configuration**: `darn library diagnose --json` +- **List actions**: `darn action list` +- **Action details**: `darn action show ` \ No newline at end of file diff --git a/docs/LIBRARY_SYSTEM.md b/docs/LIBRARY_SYSTEM.md new file mode 100644 index 0000000..dd0d976 --- /dev/null +++ b/docs/LIBRARY_SYSTEM.md @@ -0,0 +1,540 @@ +# Darn Library System Documentation + +This document explains how the darn library system works, from basic concepts to technical implementation details. + +## Overview + +The darn library system provides a structured way to organize and manage reusable components for security remediation workflows. It consists of four main types of components: + +- **Actions** - Executable operations (CLI commands, file creation) +- **Templates** - Reusable content patterns +- **Configs** - Configuration presets +- **Mappings** - Rules that map security findings to remediation actions + +## Library Structure + +### Standard Directory Layout + +``` +library/ +β”œβ”€β”€ actions/ # Action definitions (.yaml files) +β”œβ”€β”€ templates/ # Template files (.txt, .md, etc.) +β”œβ”€β”€ configs/ # Configuration files (.yaml, .json) +└── mappings/ # Mapping rules (.yaml files) +``` + +### Library Path Resolution + +The library path is resolved using this precedence order: + +1. **Command line** - `--library-path` flag +2. **Environment** - `DARN_HOME` variable (for testing) +3. **Global config** - `~/.darn/config.yaml` setting +4. **Default** - `~/.darn/library` + +Example: +```bash +# Use specific library +darn --library-path /custom/lib action list + +# Use temporary library for testing +DARN_HOME=/tmp/test-lib darn action list + +# Use global library (default) +darn action list +``` + +## Components Deep Dive + +### Actions + +Actions are the core executable units in darn. They define how to perform specific remediation tasks. + +#### Action Types + +**1. CLI Actions** (`type: "cli"`) +- Execute command-line tools +- Support parameter templating +- Validate commands exist in PATH + +**2. File Actions** (`type: "file"`) +- Create files from templates +- Support directory creation +- Safe path handling + +#### Action Definition Structure + +```yaml +# actions/example-action.yaml +name: "example-action" +description: "Example action that demonstrates the structure" +type: "cli" +command: "echo" +args: ["Hello {{.name}}!"] + +parameters: + - name: "name" + type: "string" + required: true + description: "Name to greet" + default: "World" + +# Optional: outputs for use by other actions +outputs: + greeting_file: "{{.working_dir}}/greeting.txt" + +# Optional: labels for categorization +labels: + category: ["demo", "example"] + complexity: ["simple"] +``` + +#### Parameter Types and Validation + +| Type | Description | Example | +|------|-------------|---------| +| `string` | Text value | `"hello world"` | +| `array` | List of values | `["item1", "item2"]` | +| `number` | Numeric value | `42` | +| `boolean` | True/false | `true` | + +Parameters support: +- **Required validation** - `required: true` +- **Default values** - `default: "value"` +- **Constraints** - Min/max for numbers, regex for strings +- **Templating** - Use `{{.param_name}}` in command args + +### Templates + +Templates are reusable content patterns used by file actions. + +#### Template Structure + +``` +templates/ +β”œβ”€β”€ security.md # Simple template +β”œβ”€β”€ workflows/ # Organized by category +β”‚ β”œβ”€β”€ github-actions.yml +β”‚ └── ci-pipeline.yml +└── licenses/ + β”œβ”€β”€ apache-2.0.txt + └── mit.txt +``` + +#### Template Syntax + +Templates use Go's `text/template` syntax: + +```markdown + +# Security Policy for {{.project_name}} + +## Reporting Vulnerabilities + +Please report security vulnerabilities to {{.security_email}}. + +{{if .has_bug_bounty}} +## Bug Bounty Program + +We offer rewards for qualifying security reports. +{{end}} + +## Supported Versions + +| Version | Supported | +|---------|-----------| +{{range .supported_versions}} +| {{.version}} | {{.status}} | +{{end}} +``` + +Used by file action: +```yaml +# actions/add-security-md.yaml +name: "add-security-md" +type: "file" +template_path: "security.md" +target_path: "SECURITY.md" +create_dirs: false + +parameters: + - name: "project_name" + type: "string" + required: true + - name: "security_email" + type: "string" + required: true + - name: "has_bug_bounty" + type: "boolean" + default: false +``` + +### Configs + +Configuration files provide reusable settings and presets. + +#### Example Config + +```yaml +# configs/github-security.yaml +name: "GitHub Security Standards" +description: "Standard security configuration for GitHub repositories" + +settings: + branch_protection: + enforce_admins: true + required_status_checks: + strict: true + contexts: ["ci/tests", "security/scan"] + required_pull_request_reviews: + required_approving_review_count: 2 + dismiss_stale_reviews: true + + security_features: + vulnerability_alerts: true + security_updates: true + secret_scanning: true + +defaults: + security_email: "security@company.com" + response_time: "24 hours" +``` + +### Mappings + +Mappings define rules that automatically select actions based on security findings. + +#### Mapping Structure + +```yaml +# mappings/security-baseline.yaml +mappings: + - id: "missing-security-policy" + condition: "security_policy == 'missing'" + action: "add-security-md" + reason: "Add required security policy documentation" + parameters: + project_name: "{{.project_name}}" + security_email: "{{.security_email}}" + + - id: "weak-branch-protection" + condition: "branch_protection == 'none' || branch_protection == 'weak'" + action: "enable-branch-protection" + reason: "Strengthen branch protection rules" + parameters: + repository: "{{.repository}}" + + - id: "complex-remediation" + condition: "security_score < 70" + mapping_ref: "comprehensive-security.yaml" + reason: "Apply comprehensive security improvements" + parameters: + baseline_config: "github-security" +``` + +#### Condition Syntax + +Conditions use CEL (Common Expression Language): + +```yaml +# Simple comparisons +condition: "mfa_enabled == false" +condition: "security_score < 50" + +# Logical operators +condition: "mfa_enabled == false && admin_users > 0" +condition: "branch_protection == 'none' || branch_protection == 'weak'" + +# String operations +condition: "security_policy.contains('incomplete')" +condition: "repository.startsWith('public-')" + +# Array operations +condition: "required_checks.size() < 2" +condition: "'security-scan' in required_checks" +``` + +#### Mapping References + +Complex remediation can be split across multiple mapping files: + +```yaml +# mappings/security-baseline.yaml +mappings: + - id: "comprehensive-fix" + condition: "needs_full_security_review == true" + mapping_ref: "detailed-security-review.yaml" + parameters: + severity_level: "{{.severity}}" +``` + +## Library Management + +### Initialization + +Create a new library with standard structure: + +```bash +# Initialize at default location (~/.darn/library) +darn library init + +# Initialize at custom location +darn library init /path/to/custom/library + +# Initialize with custom subdirectory names +darn library init --actions-dir my-actions --templates-dir my-templates +``` + +### Synchronization + +Update library with latest embedded defaults: + +```bash +# Sync global library +darn library sync + +# Preview changes without applying +darn library sync --dry-run + +# Sync specific library +darn library sync --library-path /custom/library + +# Use only local defaults (no remote fetch) +darn library sync --local-only +``` + +### Management Commands + +```bash +# Set global library path +darn library set-global /path/to/library + +# Diagnose configuration issues +darn library diagnose + +# Update from source directory +darn library update /source/directory + +# List available actions +darn action list + +# Show action details +darn action show create-file +``` + +## Technical Implementation + +### Library Manager + +The `library.Manager` handles path resolution and validation: + +```go +type Manager struct { + globalLibraryPath string + cmdLineLibraryPath string + verboseLogging bool + validatedPaths map[string]bool +} + +// Resolve library path with clear precedence +func (m *Manager) ResolveLibraryPath() (*LibraryInfo, error) + +// Validate library structure +func (m *Manager) validateLibraryPath(path, source string) *LibraryInfo +``` + +### Action Factory + +The action factory creates action instances from definitions: + +```go +type Factory struct { + actionCreators map[string]ActionCreator + context ActionContext +} + +// Register action types +func (f *Factory) RegisterDefaultTypes() + +// Create action from config +func (f *Factory) Create(config Config) (Action, error) +``` + +### Execution Flow + +1. **Library Resolution** + - Resolve library path using precedence rules + - Validate library structure exists + - Load action definitions + +2. **Action Creation** + - Parse action YAML definition + - Validate required parameters + - Create action instance via factory + +3. **Parameter Processing** + - Merge defaults with provided values + - Validate parameter types and constraints + - Process template variables + +4. **Execution** + - CLI actions: Validate command exists, execute + - File actions: Resolve template, create target file + +### Error Handling + +The system provides detailed error messages: + +- **Library not found** - Clear resolution order and suggestions +- **Invalid action** - Specific validation failures +- **Missing parameters** - List of required parameters +- **Command not found** - PATH validation and installation hints + +### Security Considerations + +- **No shell injection** - CLI actions use exec.Command, not shell +- **Path validation** - Prevent directory traversal attacks +- **Input sanitization** - Template variables are escaped +- **Command validation** - Check commands exist before execution + +## Best Practices + +### Organizing Actions + +``` +actions/ +β”œβ”€β”€ security/ +β”‚ β”œβ”€β”€ add-security-md.yaml +β”‚ β”œβ”€β”€ enable-mfa.yaml +β”‚ └── setup-scanning.yaml +β”œβ”€β”€ compliance/ +β”‚ β”œβ”€β”€ add-license.yaml +β”‚ └── setup-governance.yaml +└── infrastructure/ + β”œβ”€β”€ setup-monitoring.yaml + └── configure-alerts.yaml +``` + +### Template Organization + +``` +templates/ +β”œβ”€β”€ policies/ +β”‚ β”œβ”€β”€ security.md +β”‚ β”œβ”€β”€ privacy.md +β”‚ └── terms.md +β”œβ”€β”€ workflows/ +β”‚ β”œβ”€β”€ ci.yml +β”‚ └── security-scan.yml +└── configs/ + β”œβ”€β”€ eslint.json + └── prettier.json +``` + +### Parameter Design + +```yaml +parameters: + # Use descriptive names + - name: "organization_name" # Good + # not: "org" # Too short + + # Provide defaults when sensible + - name: "response_time" + default: "24 hours" + + # Use appropriate types + - name: "admin_emails" + type: "array" # For multiple values + # not: "string" # Comma-separated string + + # Add descriptions + - name: "severity_threshold" + description: "Minimum severity level to trigger alerts (1-10)" + type: "number" +``` + +### Mapping Strategy + +```yaml +# Group related checks +mappings: + # Basic security hygiene + - id: "security-policy-check" + condition: "security_policy == 'missing'" + # ... + + - id: "license-check" + condition: "license == 'missing'" + # ... + + # Advanced security (separate file) + - id: "advanced-security" + condition: "security_score < 80" + mapping_ref: "advanced-security.yaml" +``` + +## Integration Examples + +### CI/CD Pipeline + +```yaml +# .github/workflows/security.yml +name: Security Remediation +on: [push, pull_request] + +jobs: + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Run Security Scan + run: security-scanner --output report.json + + - name: Generate Remediation Plan + run: | + darn library sync + darnit plan generate report.json \ + --mapping security-baseline.yaml \ + --output remediation-plan.yaml + + - name: Execute Remediation + run: darnit plan execute remediation-plan.yaml +``` + +### Custom Action Development + +```bash +# 1. Create action definition +cat > ~/.darn/library/actions/custom-security-check.yaml << EOF +name: "custom-security-check" +description: "Run custom security validation" +type: "cli" +command: "my-security-tool" +args: ["--config", "{{.config_file}}", "--output", "{{.output_file}}"] + +parameters: + - name: "config_file" + type: "string" + required: true + - name: "output_file" + type: "string" + default: "security-report.json" +EOF + +# 2. Test the action +darn action show custom-security-check + +# 3. Use in mapping +cat > ~/.darn/library/mappings/custom-checks.yaml << EOF +mappings: + - id: "run-custom-check" + condition: "requires_custom_validation == true" + action: "custom-security-check" + parameters: + config_file: "{{.security_config}}" +EOF +``` + +This documentation provides a complete understanding of how the darn library system works, from basic usage to advanced customization and integration patterns. \ No newline at end of file diff --git a/internal/core/config/config.go b/internal/core/config/config.go index 200fa19..9b0c002 100644 --- a/internal/core/config/config.go +++ b/internal/core/config/config.go @@ -15,6 +15,7 @@ import ( "time" "github.com/kusari-oss/darn/internal/core/action" + "github.com/kusari-oss/darn/internal/core/library" "gopkg.in/yaml.v3" ) @@ -40,6 +41,9 @@ type Config struct { UseGlobal bool `yaml:"use_global"` UseLocal bool `yaml:"use_local"` GlobalFirst bool `yaml:"global_first"` + + // Runtime library manager + LibraryManager *library.Manager `yaml:"-"` } // State holds the runtime state of darn @@ -186,6 +190,9 @@ func LoadConfig(cmdLineLibraryPath string, globalConfigPathOverride string) (*Co // All paths that are tilde-expanded by ExpandPathWithTilde will be absolute. // Paths that are not tilde-expanded (e.g. already absolute, or relative without tilde) remain as is. + // Initialize the library manager + config.LibraryManager = library.NewManager(config.LibraryPath, config.CmdLineLibraryPath, false) + return config, nil } @@ -414,3 +421,51 @@ func loadActionsFromDir(actionsDir string) (map[string]action.Config, error) { return actions, nil } + +// GetLibraryInfo resolves and validates the library path using the library manager +func (c *Config) GetLibraryInfo() (*library.LibraryInfo, error) { + if c.LibraryManager == nil { + c.LibraryManager = library.NewManager(c.LibraryPath, c.CmdLineLibraryPath, false) + } + return c.LibraryManager.ResolveLibraryPath() +} + +// SetVerboseLibraryLogging enables verbose logging for library operations +func (c *Config) SetVerboseLibraryLogging(verbose bool) { + if c.LibraryManager == nil { + c.LibraryManager = library.NewManager(c.LibraryPath, c.CmdLineLibraryPath, verbose) + } else { + // Recreate with new verbosity setting + c.LibraryManager = library.NewManager(c.LibraryPath, c.CmdLineLibraryPath, verbose) + } +} + +// ValidateLibrarySetup validates that the configured library is usable +func (c *Config) ValidateLibrarySetup() error { + info, err := c.GetLibraryInfo() + if err != nil { + return fmt.Errorf("library validation failed: %w", err) + } + + if !info.Valid { + return fmt.Errorf("library at %s is not valid: %s", info.Path, strings.Join(info.Errors, ", ")) + } + + return nil +} + +// GetLibraryDiagnostics returns diagnostic information about the library system +func (c *Config) GetLibraryDiagnostics() map[string]interface{} { + if c.LibraryManager == nil { + c.LibraryManager = library.NewManager(c.LibraryPath, c.CmdLineLibraryPath, false) + } + return c.LibraryManager.GetDiagnostics() +} + +// ValidateCommand validates that a command exists and is executable +func (c *Config) ValidateCommand(command string) error { + if c.LibraryManager == nil { + c.LibraryManager = library.NewManager(c.LibraryPath, c.CmdLineLibraryPath, false) + } + return c.LibraryManager.ValidateShellCommand(command) +} diff --git a/internal/core/library/manager.go b/internal/core/library/manager.go new file mode 100644 index 0000000..27a6e73 --- /dev/null +++ b/internal/core/library/manager.go @@ -0,0 +1,318 @@ +// SPDX-License-Identifier: Apache-2.0 + +package library + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +// Manager provides robust library path resolution and validation +type Manager struct { + // Configuration + globalLibraryPath string + cmdLineLibraryPath string + verboseLogging bool + + // Cached validation results + validatedPaths map[string]bool +} + +// LibraryInfo contains information about a resolved library +type LibraryInfo struct { + Path string + Source string // "cmdline", "config", "default", "env" + ActionsDir string + TemplatesDir string + ConfigsDir string + MappingsDir string + Exists bool + Valid bool + Errors []string +} + +// NewManager creates a new library manager +func NewManager(globalLibraryPath, cmdLineLibraryPath string, verbose bool) *Manager { + return &Manager{ + globalLibraryPath: globalLibraryPath, + cmdLineLibraryPath: cmdLineLibraryPath, + verboseLogging: verbose, + validatedPaths: make(map[string]bool), + } +} + +// ResolveLibraryPath determines the library path using clear precedence rules +func (m *Manager) ResolveLibraryPath() (*LibraryInfo, error) { + // Precedence order (highest to lowest): + // 1. Command line flag (--library-path) + // 2. DARN_HOME environment variable (for testing) + // 3. Global config file setting + // 4. Default global library path + + candidates := []struct { + path string + source string + desc string + }{ + {m.cmdLineLibraryPath, "cmdline", "command line --library-path flag"}, + {os.Getenv("DARN_HOME"), "env", "DARN_HOME environment variable"}, + {m.globalLibraryPath, "config", "global configuration file"}, + {expandPath("~/.darn/library"), "default", "default global library"}, + } + + var lastInfo *LibraryInfo + var errors []string + + for _, candidate := range candidates { + if candidate.path == "" { + continue + } + + expandedPath := expandPath(candidate.path) + if m.verboseLogging { + fmt.Printf("Checking library path from %s: %s\n", candidate.desc, expandedPath) + } + + info := m.validateLibraryPath(expandedPath, candidate.source) + lastInfo = info + + if info.Valid { + if m.verboseLogging { + fmt.Printf("βœ“ Using library from %s: %s\n", candidate.desc, expandedPath) + } + return info, nil + } + + errorMsg := fmt.Sprintf("%s (%s): %s", candidate.desc, expandedPath, strings.Join(info.Errors, ", ")) + errors = append(errors, errorMsg) + + if m.verboseLogging { + fmt.Printf("βœ— Invalid library at %s: %s\n", expandedPath, strings.Join(info.Errors, ", ")) + } + } + + // If we get here, no valid library was found + if lastInfo != nil { + lastInfo.Errors = errors + return lastInfo, fmt.Errorf("no valid library found. Tried:\n - %s", strings.Join(errors, "\n - ")) + } + + return nil, fmt.Errorf("no library paths configured") +} + +// validateLibraryPath validates that a library path exists and has required structure +func (m *Manager) validateLibraryPath(path, source string) *LibraryInfo { + info := &LibraryInfo{ + Path: path, + Source: source, + ActionsDir: filepath.Join(path, "actions"), + TemplatesDir: filepath.Join(path, "templates"), + ConfigsDir: filepath.Join(path, "configs"), + MappingsDir: filepath.Join(path, "mappings"), + Exists: false, + Valid: false, + Errors: []string{}, + } + + // Check if main path exists + if _, err := os.Stat(path); os.IsNotExist(err) { + info.Errors = append(info.Errors, "library directory does not exist") + return info + } + info.Exists = true + + // Check required subdirectories + requiredDirs := map[string]string{ + "actions": info.ActionsDir, + "templates": info.TemplatesDir, + "configs": info.ConfigsDir, + "mappings": info.MappingsDir, + } + + missingDirs := []string{} + for name, dirPath := range requiredDirs { + if _, err := os.Stat(dirPath); os.IsNotExist(err) { + missingDirs = append(missingDirs, name) + } + } + + if len(missingDirs) > 0 { + info.Errors = append(info.Errors, fmt.Sprintf("missing required subdirectories: %s", strings.Join(missingDirs, ", "))) + return info + } + + // Check if we can read from the directories + for name, dirPath := range requiredDirs { + if !isReadable(dirPath) { + info.Errors = append(info.Errors, fmt.Sprintf("cannot read %s directory: %s", name, dirPath)) + return info + } + } + + info.Valid = true + return info +} + +// ValidateShellCommand validates that a shell command exists and is executable +func (m *Manager) ValidateShellCommand(command string) error { + if command == "" { + return fmt.Errorf("empty command") + } + + // Handle platform-specific executables + if runtime.GOOS == "windows" { + // On Windows, try with common extensions if no extension provided + if !strings.Contains(command, ".") { + for _, ext := range []string{".exe", ".bat", ".cmd"} { + if _, err := exec.LookPath(command + ext); err == nil { + return nil + } + } + } + } + + // Standard PATH lookup + _, err := exec.LookPath(command) + if err != nil { + return fmt.Errorf("command '%s' not found in PATH: %w", command, err) + } + + return nil +} + +// CreateLibraryStructure creates a new library structure at the given path +func (m *Manager) CreateLibraryStructure(path string) error { + expandedPath := expandPath(path) + + if m.verboseLogging { + fmt.Printf("Creating library structure at: %s\n", expandedPath) + } + + // Create main directory + if err := os.MkdirAll(expandedPath, 0755); err != nil { + return fmt.Errorf("failed to create library directory: %w", err) + } + + // Create required subdirectories + subdirs := []string{"actions", "templates", "configs", "mappings"} + for _, subdir := range subdirs { + subdirPath := filepath.Join(expandedPath, subdir) + if err := os.MkdirAll(subdirPath, 0755); err != nil { + return fmt.Errorf("failed to create %s directory: %w", subdir, err) + } + if m.verboseLogging { + fmt.Printf("Created directory: %s\n", subdirPath) + } + } + + return nil +} + +// ListAvailableActions returns a list of available actions in the library +func (m *Manager) ListAvailableActions(libraryPath string) ([]string, error) { + actionsDir := filepath.Join(libraryPath, "actions") + + entries, err := os.ReadDir(actionsDir) + if err != nil { + return nil, fmt.Errorf("failed to read actions directory: %w", err) + } + + var actions []string + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".yaml") { + // Remove .yaml extension + actionName := strings.TrimSuffix(entry.Name(), ".yaml") + actions = append(actions, actionName) + } + } + + return actions, nil +} + +// expandPath expands ~ to home directory and handles DARN_HOME for testing +func expandPath(path string) string { + if path == "" { + return "" + } + + // Handle DARN_HOME environment variable for testing + if darnHome := os.Getenv("DARN_HOME"); darnHome != "" { + if path == "~" || path == "~/" { + return darnHome + } + if strings.HasPrefix(path, "~/") { + return filepath.Join(darnHome, path[2:]) + } + if strings.HasPrefix(path, "~/.darn/library") { + return filepath.Join(darnHome, ".darn", "library") + } + } + + // Standard home directory expansion + if strings.HasPrefix(path, "~/") { + home, err := os.UserHomeDir() + if err != nil { + // Return original path if we can't expand + return path + } + return filepath.Join(home, path[2:]) + } + + if path == "~" { + home, err := os.UserHomeDir() + if err != nil { + return path + } + return home + } + + return path +} + +// isReadable checks if a directory is readable +func isReadable(path string) bool { + file, err := os.Open(path) + if err != nil { + return false + } + defer file.Close() + + // Try to read the directory + _, err = file.Readdir(1) + // EOF is expected for empty directories + return err == nil || err.Error() == "EOF" +} + +// GetDiagnostics returns diagnostic information about the library system +func (m *Manager) GetDiagnostics() map[string]interface{} { + diagnostics := make(map[string]interface{}) + + diagnostics["cmdline_library_path"] = m.cmdLineLibraryPath + diagnostics["global_library_path"] = m.globalLibraryPath + diagnostics["darn_home"] = os.Getenv("DARN_HOME") + + if home, err := os.UserHomeDir(); err == nil { + diagnostics["user_home"] = home + } else { + diagnostics["user_home_error"] = err.Error() + } + + // Test library resolution + if info, err := m.ResolveLibraryPath(); err == nil { + diagnostics["resolved_library"] = map[string]interface{}{ + "path": info.Path, + "source": info.Source, + "valid": info.Valid, + "exists": info.Exists, + "errors": info.Errors, + } + } else { + diagnostics["resolution_error"] = err.Error() + } + + return diagnostics +} \ No newline at end of file diff --git a/internal/core/library/updater.go b/internal/core/library/updater.go index 915cc03..f005817 100644 --- a/internal/core/library/updater.go +++ b/internal/core/library/updater.go @@ -9,8 +9,6 @@ import ( "path/filepath" "strings" "time" - - "github.com/kusari-oss/darn/internal/core/config" ) // Updater handles the updating of library files @@ -251,25 +249,10 @@ func (u *Updater) copyFile(sourcePath, targetPath string) error { return os.Chmod(targetPath, sourceInfo.Mode()) } -// updateStateFile updates the state file with the latest update time +// updateStateFile updates a simple timestamp file to track updates func (u *Updater) updateStateFile() error { - // Try to load existing state - state, err := config.LoadState(u.libraryPath) - if os.IsNotExist(err) { - // Create a new state if none exists - state = &config.State{ - ProjectDir: u.libraryPath, - LibraryInUse: u.libraryPath, - InitializedAt: time.Now().Format(time.RFC3339), - Version: "unknown", // Would be set from version info - } - } else if err != nil { - return err - } - - // Update the last updated time - state.LastUpdated = time.Now().Format(time.RFC3339) - - // Save the state - return config.SaveState(state, u.libraryPath) + stateFile := filepath.Join(u.libraryPath, ".last_updated") + timestamp := time.Now().Format(time.RFC3339) + + return os.WriteFile(stateFile, []byte(timestamp), 0644) } diff --git a/internal/darn/resolver/resolver.go b/internal/darn/resolver/resolver.go index 30a5723..ce1c05f 100644 --- a/internal/darn/resolver/resolver.go +++ b/internal/darn/resolver/resolver.go @@ -18,6 +18,7 @@ import ( "gopkg.in/yaml.v3" "github.com/kusari-oss/darn/internal/core/action" + "github.com/kusari-oss/darn/internal/core/library" ) // Resolver handles finding and loading actions based on configuration @@ -404,3 +405,33 @@ func hasAnyMatchingValue(actionValues, selectorValues []string) bool { } return false } + +// ValidateLibraryPaths validates that all configured library paths exist and are accessible +func (r *Resolver) ValidateLibraryPaths() []error { + var errors []error + + for _, path := range r.actionPaths { + if _, err := os.Stat(path); os.IsNotExist(err) { + errors = append(errors, fmt.Errorf("action path does not exist: %s", path)) + } else if err != nil { + errors = append(errors, fmt.Errorf("cannot access action path %s: %w", path, err)) + } + } + + return errors +} + +// ValidateActionCommand validates that an action's CLI command exists and is executable +func (r *Resolver) ValidateActionCommand(actionConfig *action.Config) error { + if actionConfig.Type != "cli" { + return nil // Only validate CLI commands + } + + if actionConfig.Command == "" { + return fmt.Errorf("action '%s' has empty command", actionConfig.Name) + } + + // Use library manager for validation + manager := library.NewManager("", "", false) + return manager.ValidateShellCommand(actionConfig.Command) +} diff --git a/internal/darnit/plan/plan_test.go b/internal/darnit/plan/plan_test.go index 90e7557..c2e571c 100644 --- a/internal/darnit/plan/plan_test.go +++ b/internal/darnit/plan/plan_test.go @@ -70,10 +70,9 @@ parameters: - name: "emails" type: "array" required: true -type: "shell" -implementation: - command: "echo" - args: ["Creating SECURITY.md for {{.name}}"]`, +type: "cli" +command: "echo" +args: ["Creating SECURITY.md for {{.name}}"]`, "enable-mfa.yaml": `# Test action name: "enable-mfa" @@ -82,10 +81,9 @@ parameters: - name: "organization" type: "string" required: true -type: "shell" -implementation: - command: "echo" - args: ["Enabling MFA for {{.organization}}"]`, +type: "cli" +command: "echo" +args: ["Enabling MFA for {{.organization}}"]`, } for filename, content := range actionFiles { diff --git a/internal/defaults/actions/create-file.yaml b/internal/defaults/actions/create-file.yaml index 9faeae7..ba66973 100644 --- a/internal/defaults/actions/create-file.yaml +++ b/internal/defaults/actions/create-file.yaml @@ -16,14 +16,7 @@ parameters: required: false default: "." description: "Directory where to create the file (defaults to current directory)" -type: "shell" -implementation: - command: "sh" - args: - - "-c" - - | - if [ ! -d "{{.directory}}" ]; then - mkdir -p "{{.directory}}" - fi - echo "{{.content}}" > "{{.directory}}/{{.filename}}" - echo "Created file: {{.directory}}/{{.filename}}" \ No newline at end of file +type: "file" +template_path: "create-file.txt" +target_path: "{{.directory}}/{{.filename}}" +create_dirs: true \ No newline at end of file diff --git a/internal/defaults/templates/create-file.txt b/internal/defaults/templates/create-file.txt new file mode 100644 index 0000000..b60cec1 --- /dev/null +++ b/internal/defaults/templates/create-file.txt @@ -0,0 +1 @@ +{{.content}} \ No newline at end of file