Просмотр исходного кода

Fix/#1082 sanitize log filenames (#1086)

James Read 4 недель назад
Родитель
Сommit
ea329c1cbe

+ 3 - 3
docs/modules/ROOT/pages/install/macos.adoc

@@ -34,11 +34,11 @@ tar -xzf OliveTin-darwin-arm64.tar.gz
 cd OliveTin-darwin-arm64
 ----
 
-== Remove the Gatekeeper quarantine
+== Gatekeeper and notarization
 
-The binary is downloaded from the internet and is not notarized by Apple, so on first run Gatekeeper blocks it with a message like _"OliveTin can't be opened because Apple cannot check it for malicious software."_
+Current release binaries are **Developer ID signed and notarized** by Apple. After extract, you should be able to run `./OliveTin` normally.
 
-Clear the quarantine attribute so it will run:
+If Gatekeeper still blocks an older (unsigned) build, or you see a prompt that Apple cannot check the binary for malicious software, clear the quarantine attribute:
 
 [source,shell]
 ----

+ 1 - 1
docs/modules/ROOT/pages/install/macos_service.adoc

@@ -2,7 +2,7 @@
 
 This option installs OliveTin as a launchd service, so it runs in the background and starts automatically. This is the macOS equivalent of running OliveTin as a Linux systemd service or a xref:install/windows_service.adoc[Windows service]. If you just want to run OliveTin as a regular application, follow the xref:install/macos.adoc[macOS install] instructions instead.
 
-Before continuing, complete the xref:install/macos.adoc[macOS install] steps (download, extract, and clear the Gatekeeper quarantine) and confirm OliveTin starts correctly by running `./OliveTin`.
+Before continuing, complete the xref:install/macos.adoc[macOS install] steps (download, extract, and confirm Gatekeeper allows the binary) and confirm OliveTin starts correctly by running `./OliveTin`.
 
 == Choose LaunchAgent or LaunchDaemon
 

+ 2 - 0
docs/modules/ROOT/pages/logs/saving.adoc

@@ -29,6 +29,8 @@ actions:
 
 From the above example, you can see there There are two types of logs - **results (.yaml)** and **output (.log)**
 
+Filenames are built from the action title, a unix timestamp, and the execution tracking ID. Characters that are unsafe in filenames (for example `/` and `\`) are replaced with `_` so titles like `Create/update Monthly Report` still write a single file. The original title is unchanged inside the results YAML.
+
 * **Results (.yaml)** - this captures almost everything that OliveTin knows about the action and looks like this.
 +
 [source,yaml]

+ 19 - 1
service/internal/executor/executor.go

@@ -1391,7 +1391,7 @@ func triggerLoop(req *ExecutionRequest) {
 }
 
 func stepSaveLog(req *ExecutionRequest) bool {
-	filename := fmt.Sprintf("%v.%v.%v", req.logEntry.ActionTitle, req.logEntry.DatetimeStarted.Unix(), req.logEntry.ExecutionTrackingID)
+	filename := fmt.Sprintf("%v.%v.%v", sanitizeLogFilename(req.logEntry.ActionTitle), req.logEntry.DatetimeStarted.Unix(), req.logEntry.ExecutionTrackingID)
 
 	saveLogResults(req, filename)
 	saveLogOutput(req, filename)
@@ -1399,6 +1399,24 @@ func stepSaveLog(req *ExecutionRequest) bool {
 	return true
 }
 
+// sanitizeLogFilename replaces characters that are unsafe in filenames so action
+// titles like "Create/update Report" do not create nested paths or fail to write.
+func sanitizeLogFilename(title string) string {
+	replacer := strings.NewReplacer(
+		"/", "_",
+		"\\", "_",
+		":", "_",
+		"*", "_",
+		"?", "_",
+		"\"", "_",
+		"<", "_",
+		">", "_",
+		"|", "_",
+	)
+
+	return replacer.Replace(title)
+}
+
 func firstNonEmpty(one, two string) string {
 	if one != "" {
 		return one

+ 90 - 0
service/internal/executor/executor_test.go

@@ -780,3 +780,93 @@ func (c *executionFinishedCollector) OnExecutionFinished(entry *InternalLogEntry
 func (c *executionFinishedCollector) OnOutputChunk(_ []byte, _ string) {}
 
 func (c *executionFinishedCollector) OnActionMapRebuilt() {}
+
+func TestSanitizeLogFilename(t *testing.T) {
+	tests := []struct {
+		title string
+		want  string
+	}{
+		{"Echo Test", "Echo Test"},
+		{"Create/update Monthly Report", "Create_update Monthly Report"},
+		{`path\with\backslashes`, "path_with_backslashes"},
+		{`a:b*c?d"e<f>g|h`, "a_b_c_d_e_f_g_h"},
+	}
+
+	for _, tt := range tests {
+		assert.Equal(t, tt.want, sanitizeLogFilename(tt.title), "title=%q", tt.title)
+	}
+}
+
+func TestStepSaveLogSanitizesSlashInTitle(t *testing.T) {
+	resultsDir := t.TempDir()
+	outputDir := t.TempDir()
+	started := time.Unix(1714333384, 0)
+	trackingID := "5e2dc9e5-b6b3-445b-bff9-c2082b0bbbb2"
+	title := "Create/update Monthly Report"
+
+	req := &ExecutionRequest{
+		Cfg: &config.Config{
+			SaveLogs: config.SaveLogsConfig{
+				ResultsDirectory: resultsDir,
+				OutputDirectory:  outputDir,
+			},
+		},
+		Binding: &ActionBinding{
+			Action: &config.Action{},
+		},
+		logEntry: &InternalLogEntry{
+			ActionTitle:         title,
+			DatetimeStarted:     started,
+			ExecutionTrackingID: trackingID,
+			Output:              "report ok",
+		},
+	}
+
+	assert.True(t, stepSaveLog(req))
+
+	expectedBase := "Create_update Monthly Report.1714333384." + trackingID
+	resultsPath := filepath.Join(resultsDir, expectedBase+".yaml")
+	outputPath := filepath.Join(outputDir, expectedBase+".log")
+
+	assert.FileExists(t, resultsPath)
+	assert.FileExists(t, outputPath)
+
+	resultsEntries, err := os.ReadDir(resultsDir)
+	assert.NoError(t, err)
+	assert.Len(t, resultsEntries, 1, "results file must be flat under resultsDirectory, not a subdirectory")
+
+	data, err := os.ReadFile(resultsPath)
+	assert.NoError(t, err)
+	assert.Contains(t, string(data), title, "YAML content keeps the original action title")
+
+	output, err := os.ReadFile(outputPath)
+	assert.NoError(t, err)
+	assert.Equal(t, "report ok", string(output))
+}
+
+func TestStepSaveLogKeepsSafeTitleFilename(t *testing.T) {
+	resultsDir := t.TempDir()
+	started := time.Unix(1714333384, 0)
+	trackingID := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
+
+	req := &ExecutionRequest{
+		Cfg: &config.Config{
+			SaveLogs: config.SaveLogsConfig{
+				ResultsDirectory: resultsDir,
+			},
+		},
+		Binding: &ActionBinding{
+			Action: &config.Action{},
+		},
+		logEntry: &InternalLogEntry{
+			ActionTitle:         "Echo Test",
+			DatetimeStarted:     started,
+			ExecutionTrackingID: trackingID,
+		},
+	}
+
+	assert.True(t, stepSaveLog(req))
+
+	expectedPath := filepath.Join(resultsDir, "Echo Test.1714333384."+trackingID+".yaml")
+	assert.FileExists(t, expectedPath)
+}