mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-09-11 04:07:47 +02:00
893 lines
27 KiB
Go
893 lines
27 KiB
Go
package e2e
|
|
|
|
import (
|
|
"archive/zip"
|
|
"context"
|
|
"database/sql"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestData holds database state for verification
|
|
type TestData struct {
|
|
Workspaces []WorkspaceRecord
|
|
Assets []AssetRecord
|
|
Vulns []VulnRecord
|
|
}
|
|
|
|
// WorkspaceRecord represents a workspace in the database
|
|
type WorkspaceRecord struct {
|
|
ID string
|
|
Name string
|
|
}
|
|
|
|
// AssetRecord represents an asset in the database
|
|
type AssetRecord struct {
|
|
ID string
|
|
Workspace string
|
|
AssetValue string
|
|
}
|
|
|
|
// VulnRecord represents a vulnerability in the database
|
|
type VulnRecord struct {
|
|
ID string
|
|
Workspace string
|
|
VulnTitle string
|
|
}
|
|
|
|
// getExamplesPath returns the path to the public/examples directory
|
|
func getExamplesPath(t *testing.T) string {
|
|
t.Helper()
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("Failed to get caller info")
|
|
}
|
|
return filepath.Join(filepath.Dir(filename), "..", "..", "public", "examples")
|
|
}
|
|
|
|
// setupTestBase creates an isolated test base folder with configuration
|
|
func setupTestBase(t *testing.T) (basePath string, workspacesPath string, cleanup func()) {
|
|
t.Helper()
|
|
|
|
// Create temp directory for test base
|
|
tempDir, err := os.MkdirTemp("", "osmedeus-test-base-*")
|
|
require.NoError(t, err)
|
|
|
|
basePath = tempDir
|
|
workspacesPath = filepath.Join(tempDir, "workspaces")
|
|
|
|
// Create necessary directories
|
|
dirs := []string{
|
|
filepath.Join(basePath, "workflows", "modules"),
|
|
filepath.Join(basePath, "workflows", "flows"),
|
|
filepath.Join(basePath, "external-binaries"),
|
|
filepath.Join(basePath, "external-data"),
|
|
filepath.Join(basePath, "external-configs"),
|
|
filepath.Join(basePath, "snapshot"),
|
|
workspacesPath,
|
|
}
|
|
for _, dir := range dirs {
|
|
require.NoError(t, os.MkdirAll(dir, 0755))
|
|
}
|
|
|
|
// Create minimal osm-settings.yaml
|
|
settingsContent := `base_folder: ` + basePath + `
|
|
environments:
|
|
external_binaries_path: "` + basePath + `/external-binaries"
|
|
external_data: "` + basePath + `/external-data"
|
|
external_configs: "` + basePath + `/external-configs"
|
|
workspaces: "` + workspacesPath + `"
|
|
workflows: "` + basePath + `/workflows"
|
|
snapshot: "` + basePath + `/snapshot"
|
|
database:
|
|
db_engine: sqlite
|
|
db_path: "` + basePath + `/database-osm.sqlite"
|
|
server:
|
|
host: "127.0.0.1"
|
|
port: 18099
|
|
simple_user_map_key:
|
|
test: test
|
|
jwt:
|
|
secret_signing_key: test-secret
|
|
expiration_minutes: 60
|
|
`
|
|
settingsPath := filepath.Join(basePath, "osm-settings.yaml")
|
|
require.NoError(t, os.WriteFile(settingsPath, []byte(settingsContent), 0644))
|
|
|
|
cleanup = func() {
|
|
_ = os.RemoveAll(tempDir)
|
|
}
|
|
|
|
return basePath, workspacesPath, cleanup
|
|
}
|
|
|
|
// createZipFromDir creates a zip file from a directory
|
|
func createZipFromDir(t *testing.T, sourceDir, destZip string) string {
|
|
t.Helper()
|
|
|
|
zipFile, err := os.Create(destZip)
|
|
require.NoError(t, err)
|
|
defer func() { _ = zipFile.Close() }()
|
|
|
|
zipWriter := zip.NewWriter(zipFile)
|
|
defer func() { _ = zipWriter.Close() }()
|
|
|
|
err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get relative path
|
|
relPath, err := filepath.Rel(sourceDir, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Skip the root directory
|
|
if relPath == "." {
|
|
return nil
|
|
}
|
|
|
|
// Create zip header
|
|
header, err := zip.FileInfoHeader(info)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
header.Name = relPath
|
|
header.Method = zip.Deflate
|
|
|
|
if info.IsDir() {
|
|
header.Name += "/"
|
|
_, err = zipWriter.CreateHeader(header)
|
|
return err
|
|
}
|
|
|
|
writer, err := zipWriter.CreateHeader(header)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
_, err = io.Copy(writer, file)
|
|
return err
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return destZip
|
|
}
|
|
|
|
// startZipServer starts an httptest server that serves a zip file
|
|
func startZipServer(t *testing.T, zipPath string) *httptest.Server {
|
|
t.Helper()
|
|
|
|
zipData, err := os.ReadFile(zipPath)
|
|
require.NoError(t, err)
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/zip")
|
|
w.Header().Set("Content-Disposition", "attachment; filename=download.zip")
|
|
if _, err := w.Write(zipData); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}))
|
|
|
|
return server
|
|
}
|
|
|
|
// createGitRepo creates a local git repository from a source directory
|
|
// The repo path ends with .git so it's recognized as a git URL
|
|
func createGitRepo(t *testing.T, sourceDir string) (repoPath string, cleanup func()) {
|
|
t.Helper()
|
|
|
|
// Create temp directory for git repo
|
|
tempDir, err := os.MkdirTemp("", "osmedeus-test-git-*")
|
|
require.NoError(t, err)
|
|
|
|
// Use a path ending with .git so it's recognized as a git URL
|
|
repoPath = filepath.Join(tempDir, "repo.git")
|
|
|
|
// Copy source to repo directory
|
|
err = copyDir(sourceDir, repoPath)
|
|
require.NoError(t, err)
|
|
|
|
// Initialize git repo
|
|
cmds := [][]string{
|
|
{"git", "init"},
|
|
{"git", "config", "user.email", "test@test.com"},
|
|
{"git", "config", "user.name", "Test"},
|
|
{"git", "add", "."},
|
|
{"git", "commit", "-m", "Initial commit"},
|
|
}
|
|
|
|
for _, args := range cmds {
|
|
cmd := exec.Command(args[0], args[1:]...)
|
|
cmd.Dir = repoPath
|
|
output, err := cmd.CombinedOutput()
|
|
require.NoError(t, err, "Git command failed: %s\nOutput: %s", args, output)
|
|
}
|
|
|
|
cleanup = func() {
|
|
_ = os.RemoveAll(tempDir)
|
|
}
|
|
|
|
return repoPath, cleanup
|
|
}
|
|
|
|
// copyDir recursively copies a directory
|
|
func copyDir(src, dst string) error {
|
|
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
relPath, err := filepath.Rel(src, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
destPath := filepath.Join(dst, relPath)
|
|
|
|
if info.IsDir() {
|
|
return os.MkdirAll(destPath, info.Mode())
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(destPath, data, info.Mode())
|
|
})
|
|
}
|
|
|
|
// seedTestDatabase seeds the test database with sample data
|
|
func seedTestDatabase(t *testing.T, log *TestLogger, dbPath string) TestData {
|
|
t.Helper()
|
|
|
|
log.Info("Seeding test database at %s", dbPath)
|
|
|
|
db, err := sql.Open("sqlite3", dbPath)
|
|
require.NoError(t, err)
|
|
defer func() { _ = db.Close() }()
|
|
|
|
// Create tables if they don't exist
|
|
createTablesSQL := `
|
|
CREATE TABLE IF NOT EXISTS workspaces (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT UNIQUE NOT NULL,
|
|
local_path TEXT,
|
|
data_source TEXT DEFAULT 'local',
|
|
total_assets INTEGER DEFAULT 0,
|
|
total_vulns INTEGER DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE TABLE IF NOT EXISTS assets (
|
|
id TEXT PRIMARY KEY,
|
|
workspace TEXT NOT NULL,
|
|
asset_value TEXT,
|
|
url TEXT,
|
|
asset_type TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE TABLE IF NOT EXISTS vulnerabilities (
|
|
id TEXT PRIMARY KEY,
|
|
workspace TEXT NOT NULL,
|
|
vuln_title TEXT,
|
|
severity TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE TABLE IF NOT EXISTS workflow_metas (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT UNIQUE NOT NULL,
|
|
kind TEXT,
|
|
file_path TEXT,
|
|
checksum TEXT,
|
|
indexed_at DATETIME,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
`
|
|
_, err = db.Exec(createTablesSQL)
|
|
require.NoError(t, err)
|
|
|
|
testData := TestData{
|
|
Workspaces: []WorkspaceRecord{
|
|
{ID: "ws-001", Name: "test-workspace-1"},
|
|
{ID: "ws-002", Name: "test-workspace-2"},
|
|
},
|
|
Assets: []AssetRecord{
|
|
{ID: "asset-001", Workspace: "test-workspace-1", AssetValue: "http://example.com"},
|
|
{ID: "asset-002", Workspace: "test-workspace-1", AssetValue: "http://api.example.com"},
|
|
{ID: "asset-003", Workspace: "test-workspace-2", AssetValue: "http://test.local"},
|
|
},
|
|
Vulns: []VulnRecord{
|
|
{ID: "vuln-001", Workspace: "test-workspace-1", VulnTitle: "SQL Injection"},
|
|
{ID: "vuln-002", Workspace: "test-workspace-1", VulnTitle: "XSS"},
|
|
},
|
|
}
|
|
|
|
// Insert workspaces
|
|
for _, ws := range testData.Workspaces {
|
|
_, err = db.Exec("INSERT OR REPLACE INTO workspaces (id, name) VALUES (?, ?)", ws.ID, ws.Name)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Insert assets
|
|
for _, asset := range testData.Assets {
|
|
_, err = db.Exec("INSERT OR REPLACE INTO assets (id, workspace, asset_value) VALUES (?, ?, ?)",
|
|
asset.ID, asset.Workspace, asset.AssetValue)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Insert vulns
|
|
for _, vuln := range testData.Vulns {
|
|
_, err = db.Exec("INSERT OR REPLACE INTO vulnerabilities (id, workspace, vuln_title) VALUES (?, ?, ?)",
|
|
vuln.ID, vuln.Workspace, vuln.VulnTitle)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
log.Success("Database seeded with %d workspaces, %d assets, %d vulns",
|
|
len(testData.Workspaces), len(testData.Assets), len(testData.Vulns))
|
|
|
|
return testData
|
|
}
|
|
|
|
// getTestData retrieves current database state
|
|
func getTestData(t *testing.T, dbPath string) TestData {
|
|
t.Helper()
|
|
|
|
db, err := sql.Open("sqlite3", dbPath)
|
|
require.NoError(t, err)
|
|
defer func() { _ = db.Close() }()
|
|
|
|
testData := TestData{}
|
|
|
|
// Get workspaces
|
|
rows, err := db.Query("SELECT id, name FROM workspaces")
|
|
require.NoError(t, err)
|
|
defer func() { _ = rows.Close() }()
|
|
for rows.Next() {
|
|
var ws WorkspaceRecord
|
|
require.NoError(t, rows.Scan(&ws.ID, &ws.Name))
|
|
testData.Workspaces = append(testData.Workspaces, ws)
|
|
}
|
|
|
|
// Get assets
|
|
rows, err = db.Query("SELECT id, workspace, asset_value FROM assets")
|
|
require.NoError(t, err)
|
|
defer func() { _ = rows.Close() }()
|
|
for rows.Next() {
|
|
var asset AssetRecord
|
|
require.NoError(t, rows.Scan(&asset.ID, &asset.Workspace, &asset.AssetValue))
|
|
testData.Assets = append(testData.Assets, asset)
|
|
}
|
|
|
|
// Get vulns
|
|
rows, err = db.Query("SELECT id, workspace, vuln_title FROM vulnerabilities")
|
|
require.NoError(t, err)
|
|
defer func() { _ = rows.Close() }()
|
|
for rows.Next() {
|
|
var vuln VulnRecord
|
|
require.NoError(t, rows.Scan(&vuln.ID, &vuln.Workspace, &vuln.VulnTitle))
|
|
testData.Vulns = append(testData.Vulns, vuln)
|
|
}
|
|
|
|
return testData
|
|
}
|
|
|
|
// verifyDatabasePreserved checks that workspace, asset, and vuln records are unchanged
|
|
func verifyDatabasePreserved(t *testing.T, log *TestLogger, dbPath string, expected TestData) {
|
|
t.Helper()
|
|
|
|
log.Info("Verifying database preservation...")
|
|
|
|
actual := getTestData(t, dbPath)
|
|
|
|
// Verify workspaces
|
|
assert.Equal(t, len(expected.Workspaces), len(actual.Workspaces),
|
|
"Workspace count mismatch: expected %d, got %d", len(expected.Workspaces), len(actual.Workspaces))
|
|
for _, expectedWs := range expected.Workspaces {
|
|
found := false
|
|
for _, actualWs := range actual.Workspaces {
|
|
if actualWs.ID == expectedWs.ID && actualWs.Name == expectedWs.Name {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Workspace not found: %s", expectedWs.Name)
|
|
}
|
|
|
|
// Verify assets
|
|
assert.Equal(t, len(expected.Assets), len(actual.Assets),
|
|
"Asset count mismatch: expected %d, got %d", len(expected.Assets), len(actual.Assets))
|
|
for _, expectedAsset := range expected.Assets {
|
|
found := false
|
|
for _, actualAsset := range actual.Assets {
|
|
if actualAsset.ID == expectedAsset.ID {
|
|
found = true
|
|
assert.Equal(t, expectedAsset.AssetValue, actualAsset.AssetValue)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Asset not found: %s", expectedAsset.ID)
|
|
}
|
|
|
|
// Verify vulns
|
|
assert.Equal(t, len(expected.Vulns), len(actual.Vulns),
|
|
"Vuln count mismatch: expected %d, got %d", len(expected.Vulns), len(actual.Vulns))
|
|
for _, expectedVuln := range expected.Vulns {
|
|
found := false
|
|
for _, actualVuln := range actual.Vulns {
|
|
if actualVuln.ID == expectedVuln.ID {
|
|
found = true
|
|
assert.Equal(t, expectedVuln.VulnTitle, actualVuln.VulnTitle)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Vuln not found: %s", expectedVuln.ID)
|
|
}
|
|
|
|
log.Success("Database preservation verified!")
|
|
}
|
|
|
|
// runInstallWithBase runs the osmedeus install command with a custom base folder
|
|
func runInstallWithBase(t *testing.T, log *TestLogger, basePath string, args ...string) (stdout, stderr string, err error) {
|
|
t.Helper()
|
|
binary := getBinaryPath(t)
|
|
|
|
// Prepend base folder flag
|
|
fullArgs := append([]string{"--base-folder", basePath}, args...)
|
|
|
|
log.Command(fullArgs...)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
defer cancel()
|
|
|
|
cmd := exec.CommandContext(ctx, binary, fullArgs...)
|
|
var stdoutBuf, stderrBuf strings.Builder
|
|
cmd.Stdout = &stdoutBuf
|
|
cmd.Stderr = &stderrBuf
|
|
|
|
err = cmd.Run()
|
|
stdout = stdoutBuf.String()
|
|
stderr = stderrBuf.String()
|
|
|
|
log.Result(stdout, stderr)
|
|
|
|
return stdout, stderr, err
|
|
}
|
|
|
|
// =============================================================================
|
|
// Test Cases
|
|
// =============================================================================
|
|
|
|
// TestInstall_WorkflowFromLocalZip tests installing workflows from a local zip file
|
|
func TestInstall_WorkflowFromLocalZip(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping install e2e test in short mode")
|
|
}
|
|
|
|
log := NewTestLogger(t)
|
|
log.Step("Test: Install Workflow from Local Zip")
|
|
|
|
// Setup test base
|
|
basePath, _, cleanup := setupTestBase(t)
|
|
defer cleanup()
|
|
|
|
// Create zip from example workflows
|
|
examplesPath := getExamplesPath(t)
|
|
workflowsExamplePath := filepath.Join(examplesPath, "osmedeus-base.example", "workflows")
|
|
|
|
if _, err := os.Stat(workflowsExamplePath); os.IsNotExist(err) {
|
|
t.Skipf("Example workflows not found at %s", workflowsExamplePath)
|
|
}
|
|
|
|
zipPath := filepath.Join(basePath, "workflows-test.zip")
|
|
createZipFromDir(t, workflowsExamplePath, zipPath)
|
|
log.Info("Created test zip at %s", zipPath)
|
|
|
|
// Run install command
|
|
stdout, stderr, err := runInstallWithBase(t, log, basePath, "install", "workflow", zipPath)
|
|
require.NoError(t, err, "Install workflow failed: %s\n%s", stdout, stderr)
|
|
|
|
// Verify workflows were installed
|
|
workflowsPath := filepath.Join(basePath, "workflows")
|
|
modulesPath := filepath.Join(workflowsPath, "modules")
|
|
flowsPath := filepath.Join(workflowsPath, "flows")
|
|
|
|
// Check modules exist
|
|
expectedModules := []string{"subdomain-enum.yaml", "port-scan.yaml", "http-probe.yaml", "screenshot.yaml", "vuln-scan.yaml"}
|
|
for _, module := range expectedModules {
|
|
modulePath := filepath.Join(modulesPath, module)
|
|
assert.FileExists(t, modulePath, "Module not installed: %s", module)
|
|
}
|
|
|
|
// Check flows exist
|
|
expectedFlows := []string{"basic-recon.yaml", "vuln-assessment.yaml", "full-scan.yaml"}
|
|
for _, flow := range expectedFlows {
|
|
flowPath := filepath.Join(flowsPath, flow)
|
|
assert.FileExists(t, flowPath, "Flow not installed: %s", flow)
|
|
}
|
|
|
|
log.Success("Workflow installation from local zip verified!")
|
|
}
|
|
|
|
// TestInstall_WorkflowFromZipURL tests installing workflows from a URL
|
|
func TestInstall_WorkflowFromZipURL(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping install e2e test in short mode")
|
|
}
|
|
|
|
log := NewTestLogger(t)
|
|
log.Step("Test: Install Workflow from Zip URL")
|
|
|
|
// Setup test base
|
|
basePath, _, cleanup := setupTestBase(t)
|
|
defer cleanup()
|
|
|
|
// Create zip from example workflows
|
|
examplesPath := getExamplesPath(t)
|
|
workflowsExamplePath := filepath.Join(examplesPath, "osmedeus-base.example", "workflows")
|
|
|
|
if _, err := os.Stat(workflowsExamplePath); os.IsNotExist(err) {
|
|
t.Skipf("Example workflows not found at %s", workflowsExamplePath)
|
|
}
|
|
|
|
zipPath := filepath.Join(basePath, "workflows-url-test.zip")
|
|
createZipFromDir(t, workflowsExamplePath, zipPath)
|
|
|
|
// Start httptest server
|
|
server := startZipServer(t, zipPath)
|
|
defer server.Close()
|
|
|
|
zipURL := server.URL + "/workflows.zip"
|
|
log.Info("Serving zip at %s", zipURL)
|
|
|
|
// Run install command
|
|
stdout, stderr, err := runInstallWithBase(t, log, basePath, "install", "workflow", zipURL)
|
|
require.NoError(t, err, "Install workflow from URL failed: %s\n%s", stdout, stderr)
|
|
|
|
// Verify workflows were installed
|
|
modulesPath := filepath.Join(basePath, "workflows", "modules")
|
|
assert.DirExists(t, modulesPath, "Modules directory not created")
|
|
|
|
files, err := os.ReadDir(modulesPath)
|
|
require.NoError(t, err)
|
|
assert.GreaterOrEqual(t, len(files), 5, "Expected at least 5 module files")
|
|
|
|
log.Success("Workflow installation from URL verified!")
|
|
}
|
|
|
|
// TestInstall_WorkflowFromGitURL tests installing workflows from a git repository
|
|
func TestInstall_WorkflowFromGitURL(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping install e2e test in short mode")
|
|
}
|
|
|
|
log := NewTestLogger(t)
|
|
log.Step("Test: Install Workflow from Git URL")
|
|
|
|
// Setup test base
|
|
basePath, _, cleanup := setupTestBase(t)
|
|
defer cleanup()
|
|
|
|
// Create git repo from example workflows
|
|
examplesPath := getExamplesPath(t)
|
|
workflowsExamplePath := filepath.Join(examplesPath, "osmedeus-base.example", "workflows")
|
|
|
|
if _, err := os.Stat(workflowsExamplePath); os.IsNotExist(err) {
|
|
t.Skipf("Example workflows not found at %s", workflowsExamplePath)
|
|
}
|
|
|
|
repoPath, gitCleanup := createGitRepo(t, workflowsExamplePath)
|
|
defer gitCleanup()
|
|
|
|
log.Info("Created git repo at %s", repoPath)
|
|
|
|
// Run install command with local git path (ends with .git so it's recognized)
|
|
stdout, stderr, err := runInstallWithBase(t, log, basePath, "install", "workflow", repoPath)
|
|
require.NoError(t, err, "Install workflow from git URL failed: %s\n%s", stdout, stderr)
|
|
|
|
// Verify workflows were installed
|
|
modulesPath := filepath.Join(basePath, "workflows", "modules")
|
|
assert.DirExists(t, modulesPath, "Modules directory not created")
|
|
|
|
files, err := os.ReadDir(modulesPath)
|
|
require.NoError(t, err)
|
|
assert.GreaterOrEqual(t, len(files), 5, "Expected at least 5 module files")
|
|
|
|
log.Success("Workflow installation from git URL verified!")
|
|
}
|
|
|
|
// TestInstall_BaseFromLocalZip tests installing base folder from a local zip file
|
|
func TestInstall_BaseFromLocalZip(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping install e2e test in short mode")
|
|
}
|
|
|
|
log := NewTestLogger(t)
|
|
log.Step("Test: Install Base from Local Zip")
|
|
|
|
// Setup test base
|
|
basePath, _, cleanup := setupTestBase(t)
|
|
defer cleanup()
|
|
|
|
// Seed database with test data
|
|
dbPath := filepath.Join(basePath, "database-osm.sqlite")
|
|
expectedData := seedTestDatabase(t, log, dbPath)
|
|
|
|
// Create zip from example base
|
|
examplesPath := getExamplesPath(t)
|
|
baseExamplePath := filepath.Join(examplesPath, "osmedeus-base.example")
|
|
|
|
if _, err := os.Stat(baseExamplePath); os.IsNotExist(err) {
|
|
t.Skipf("Example base not found at %s", baseExamplePath)
|
|
}
|
|
|
|
zipPath := filepath.Join(os.TempDir(), "base-test.zip")
|
|
createZipFromDir(t, baseExamplePath, zipPath)
|
|
defer func() { _ = os.Remove(zipPath) }()
|
|
log.Info("Created test zip at %s", zipPath)
|
|
|
|
// Run install base command
|
|
stdout, stderr, err := runInstallWithBase(t, log, basePath, "install", "base", zipPath)
|
|
require.NoError(t, err, "Install base failed: %s\n%s", stdout, stderr)
|
|
|
|
// Verify base folder structure
|
|
assert.DirExists(t, filepath.Join(basePath, "workflows", "modules"))
|
|
assert.DirExists(t, filepath.Join(basePath, "workflows", "flows"))
|
|
assert.DirExists(t, filepath.Join(basePath, "external-binaries"))
|
|
assert.DirExists(t, filepath.Join(basePath, "external-data"))
|
|
assert.FileExists(t, filepath.Join(basePath, "osm-settings.yaml"))
|
|
|
|
// CRITICAL: Verify database was preserved
|
|
verifyDatabasePreserved(t, log, dbPath, expectedData)
|
|
|
|
log.Success("Base installation from local zip verified with database preservation!")
|
|
}
|
|
|
|
// TestInstall_BaseFromZipURL tests installing base folder from a URL
|
|
func TestInstall_BaseFromZipURL(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping install e2e test in short mode")
|
|
}
|
|
|
|
log := NewTestLogger(t)
|
|
log.Step("Test: Install Base from Zip URL")
|
|
|
|
// Setup test base
|
|
basePath, _, cleanup := setupTestBase(t)
|
|
defer cleanup()
|
|
|
|
// Seed database with test data
|
|
dbPath := filepath.Join(basePath, "database-osm.sqlite")
|
|
expectedData := seedTestDatabase(t, log, dbPath)
|
|
|
|
// Create zip from example base
|
|
examplesPath := getExamplesPath(t)
|
|
baseExamplePath := filepath.Join(examplesPath, "osmedeus-base.example")
|
|
|
|
if _, err := os.Stat(baseExamplePath); os.IsNotExist(err) {
|
|
t.Skipf("Example base not found at %s", baseExamplePath)
|
|
}
|
|
|
|
zipPath := filepath.Join(os.TempDir(), "base-url-test.zip")
|
|
createZipFromDir(t, baseExamplePath, zipPath)
|
|
defer func() { _ = os.Remove(zipPath) }()
|
|
|
|
// Start httptest server
|
|
server := startZipServer(t, zipPath)
|
|
defer server.Close()
|
|
|
|
zipURL := server.URL + "/base.zip"
|
|
log.Info("Serving zip at %s", zipURL)
|
|
|
|
// Run install base command
|
|
stdout, stderr, err := runInstallWithBase(t, log, basePath, "install", "base", zipURL)
|
|
require.NoError(t, err, "Install base from URL failed: %s\n%s", stdout, stderr)
|
|
|
|
// Verify base folder structure
|
|
assert.DirExists(t, filepath.Join(basePath, "workflows", "modules"))
|
|
assert.FileExists(t, filepath.Join(basePath, "osm-settings.yaml"))
|
|
|
|
// CRITICAL: Verify database was preserved
|
|
verifyDatabasePreserved(t, log, dbPath, expectedData)
|
|
|
|
log.Success("Base installation from URL verified with database preservation!")
|
|
}
|
|
|
|
// TestInstall_BaseFromGitURL tests installing base folder from a git repository
|
|
func TestInstall_BaseFromGitURL(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping install e2e test in short mode")
|
|
}
|
|
|
|
log := NewTestLogger(t)
|
|
log.Step("Test: Install Base from Git URL")
|
|
|
|
// Setup test base
|
|
basePath, _, cleanup := setupTestBase(t)
|
|
defer cleanup()
|
|
|
|
// Seed database with test data
|
|
dbPath := filepath.Join(basePath, "database-osm.sqlite")
|
|
expectedData := seedTestDatabase(t, log, dbPath)
|
|
|
|
// Create git repo from example base
|
|
examplesPath := getExamplesPath(t)
|
|
baseExamplePath := filepath.Join(examplesPath, "osmedeus-base.example")
|
|
|
|
if _, err := os.Stat(baseExamplePath); os.IsNotExist(err) {
|
|
t.Skipf("Example base not found at %s", baseExamplePath)
|
|
}
|
|
|
|
repoPath, gitCleanup := createGitRepo(t, baseExamplePath)
|
|
defer gitCleanup()
|
|
|
|
log.Info("Created git repo at %s", repoPath)
|
|
|
|
// Run install base command with local git path (ends with .git so it's recognized)
|
|
stdout, stderr, err := runInstallWithBase(t, log, basePath, "install", "base", repoPath)
|
|
require.NoError(t, err, "Install base from git URL failed: %s\n%s", stdout, stderr)
|
|
|
|
// Verify base folder structure
|
|
assert.DirExists(t, filepath.Join(basePath, "workflows", "modules"))
|
|
assert.FileExists(t, filepath.Join(basePath, "osm-settings.yaml"))
|
|
|
|
// CRITICAL: Verify database was preserved
|
|
verifyDatabasePreserved(t, log, dbPath, expectedData)
|
|
|
|
log.Success("Base installation from git URL verified with database preservation!")
|
|
}
|
|
|
|
func TestInstall_BinaryAutoInitBaseSample(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping install e2e test in short mode")
|
|
}
|
|
|
|
log := NewTestLogger(t)
|
|
log.Step("Test: Install Binary auto-initializes base sample")
|
|
|
|
basePath := t.TempDir()
|
|
|
|
_, _, _ = runInstallWithBase(t, log, basePath, "install", "binary", "--name", "nuclei", "--check")
|
|
|
|
assert.DirExists(t, filepath.Join(basePath, "workflows", "modules"))
|
|
}
|
|
|
|
// TestInstall_DatabasePreservation is a comprehensive test for database preservation
|
|
func TestInstall_DatabasePreservation(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping install e2e test in short mode")
|
|
}
|
|
|
|
log := NewTestLogger(t)
|
|
log.Step("Test: Comprehensive Database Preservation Verification")
|
|
|
|
// Setup test base
|
|
basePath, _, cleanup := setupTestBase(t)
|
|
defer cleanup()
|
|
|
|
dbPath := filepath.Join(basePath, "database-osm.sqlite")
|
|
|
|
// Step 1: Seed database with specific test data
|
|
log.Info("Step 1: Seeding database with test data...")
|
|
expectedData := seedTestDatabase(t, log, dbPath)
|
|
|
|
// Add workflow_metas entries (these should change after install)
|
|
db, err := sql.Open("sqlite3", dbPath)
|
|
require.NoError(t, err)
|
|
_, err = db.Exec("INSERT INTO workflow_metas (id, name, kind) VALUES ('old-wf-1', 'old-workflow', 'module')")
|
|
require.NoError(t, err)
|
|
_ = db.Close()
|
|
|
|
// Step 2: Record initial state
|
|
log.Info("Step 2: Recording initial database state...")
|
|
initialData := getTestData(t, dbPath)
|
|
log.Info("Initial state: %d workspaces, %d assets, %d vulns",
|
|
len(initialData.Workspaces), len(initialData.Assets), len(initialData.Vulns))
|
|
|
|
// Step 3: Create and install new base
|
|
log.Info("Step 3: Installing new base folder...")
|
|
examplesPath := getExamplesPath(t)
|
|
baseExamplePath := filepath.Join(examplesPath, "osmedeus-base.example")
|
|
|
|
if _, err := os.Stat(baseExamplePath); os.IsNotExist(err) {
|
|
t.Skipf("Example base not found at %s", baseExamplePath)
|
|
}
|
|
|
|
zipPath := filepath.Join(os.TempDir(), "base-preservation-test.zip")
|
|
createZipFromDir(t, baseExamplePath, zipPath)
|
|
defer func() { _ = os.Remove(zipPath) }()
|
|
|
|
stdout, stderr, err := runInstallWithBase(t, log, basePath, "install", "base", zipPath)
|
|
require.NoError(t, err, "Install base failed: %s\n%s", stdout, stderr)
|
|
|
|
// Step 4: Verify exact same records exist
|
|
log.Info("Step 4: Verifying database records...")
|
|
verifyDatabasePreserved(t, log, dbPath, expectedData)
|
|
|
|
// Step 5: Verify workflow table can be updated (new workflows should be indexed)
|
|
log.Info("Step 5: Verifying workflow indexing capability...")
|
|
|
|
// The old workflow_metas entry should have been removed when base was replaced
|
|
// New workflows from the installed base should be indexable
|
|
// (Actual re-indexing happens via 'osmedeus workflow refresh' or server startup)
|
|
|
|
// Verify the new osm-settings.yaml exists
|
|
assert.FileExists(t, filepath.Join(basePath, "osm-settings.yaml"))
|
|
|
|
// Step 6: Final verification
|
|
finalData := getTestData(t, dbPath)
|
|
assert.Equal(t, len(expectedData.Workspaces), len(finalData.Workspaces),
|
|
"Workspace count changed after install")
|
|
assert.Equal(t, len(expectedData.Assets), len(finalData.Assets),
|
|
"Asset count changed after install")
|
|
assert.Equal(t, len(expectedData.Vulns), len(finalData.Vulns),
|
|
"Vuln count changed after install")
|
|
|
|
log.Success("Comprehensive database preservation test passed!")
|
|
log.Info("Verified: Workspaces, Assets, and Vulnerabilities remain unchanged")
|
|
log.Info("Verified: Base folder structure was updated")
|
|
log.Info("Verified: WorkflowMeta table can be re-indexed independently")
|
|
}
|
|
|
|
// TestInstall_WorkflowListAfterInstall verifies that workflow list command works after install
|
|
func TestInstall_WorkflowListAfterInstall(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping install e2e test in short mode")
|
|
}
|
|
|
|
log := NewTestLogger(t)
|
|
log.Step("Test: Workflow List After Install")
|
|
|
|
// Setup test base
|
|
basePath, _, cleanup := setupTestBase(t)
|
|
defer cleanup()
|
|
|
|
// Create and install workflows
|
|
examplesPath := getExamplesPath(t)
|
|
workflowsExamplePath := filepath.Join(examplesPath, "osmedeus-base.example", "workflows")
|
|
|
|
if _, err := os.Stat(workflowsExamplePath); os.IsNotExist(err) {
|
|
t.Skipf("Example workflows not found at %s", workflowsExamplePath)
|
|
}
|
|
|
|
zipPath := filepath.Join(basePath, "workflows-list-test.zip")
|
|
createZipFromDir(t, workflowsExamplePath, zipPath)
|
|
|
|
// Install workflows
|
|
_, _, err := runInstallWithBase(t, log, basePath, "install", "workflow", zipPath)
|
|
require.NoError(t, err)
|
|
|
|
// Run workflow list command
|
|
stdout, stderr, _ := runInstallWithBase(t, log, basePath, "workflow", "list")
|
|
// Note: workflow list might return error if no workflows are indexed yet
|
|
// but we're checking that it doesn't crash
|
|
log.Info("Workflow list output: %s", stdout)
|
|
if stderr != "" {
|
|
log.Info("Workflow list stderr: %s", stderr)
|
|
}
|
|
|
|
// The command should at least not panic
|
|
// Success if we got here without a panic
|
|
|
|
log.Success("Workflow list command executed successfully after install!")
|
|
}
|