406 lines
9.4 KiB
Go
406 lines
9.4 KiB
Go
// Package review provides app review and validation services
|
|
package review
|
|
|
|
import (
|
|
"archive/zip"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"omixlab.com/mosis-portal/internal/database"
|
|
"omixlab.com/mosis-portal/pkg/mospkg"
|
|
)
|
|
|
|
// ReviewFlag represents a security or quality flag
|
|
type ReviewFlag struct {
|
|
Type string `json:"type"`
|
|
Severity string `json:"severity"` // "info", "warning", "critical"
|
|
Reason string `json:"reason"`
|
|
File string `json:"file,omitempty"`
|
|
Line int `json:"line,omitempty"`
|
|
}
|
|
|
|
// FullValidationResult extends ValidationResult with security flags
|
|
type FullValidationResult struct {
|
|
*mospkg.ValidationResult
|
|
Flags []ReviewFlag `json:"flags,omitempty"`
|
|
RequiresManual bool `json:"requires_manual"`
|
|
AutoApprovable bool `json:"auto_approvable"`
|
|
ValidationTimeMs int64 `json:"validation_time_ms"`
|
|
}
|
|
|
|
// Service handles app review operations
|
|
type Service struct {
|
|
db *database.DB
|
|
}
|
|
|
|
// New creates a new review service
|
|
func New(db *database.DB) *Service {
|
|
return &Service{db: db}
|
|
}
|
|
|
|
// ValidatePackage performs full validation on a package
|
|
func (s *Service) ValidatePackage(packagePath string) (*FullValidationResult, error) {
|
|
start := time.Now()
|
|
|
|
// Run basic validation
|
|
basicResult, err := mospkg.ValidatePackage(packagePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := &FullValidationResult{
|
|
ValidationResult: basicResult,
|
|
Flags: []ReviewFlag{},
|
|
AutoApprovable: true,
|
|
}
|
|
|
|
// If basic validation failed, no need to continue
|
|
if !basicResult.Valid {
|
|
result.AutoApprovable = false
|
|
result.ValidationTimeMs = time.Since(start).Milliseconds()
|
|
return result, nil
|
|
}
|
|
|
|
// Run security analysis (Tier 3)
|
|
flags, err := s.analyzeSecurityRisks(packagePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.Flags = append(result.Flags, flags...)
|
|
|
|
// Run quality checks (Tier 4)
|
|
qualityFlags := s.checkQuality(basicResult.Manifest)
|
|
result.Flags = append(result.Flags, qualityFlags...)
|
|
|
|
// Determine if manual review is required
|
|
result.RequiresManual = s.requiresManualReview(result)
|
|
if result.RequiresManual {
|
|
result.AutoApprovable = false
|
|
}
|
|
|
|
// Check for critical flags
|
|
for _, flag := range result.Flags {
|
|
if flag.Severity == "critical" {
|
|
result.AutoApprovable = false
|
|
}
|
|
}
|
|
|
|
result.ValidationTimeMs = time.Since(start).Milliseconds()
|
|
return result, nil
|
|
}
|
|
|
|
// SubmitForReview submits a version for review
|
|
func (s *Service) SubmitForReview(ctx context.Context, versionID string) error {
|
|
return s.db.UpdateVersionStatus(ctx, versionID, "in_review")
|
|
}
|
|
|
|
// ApproveVersion approves a version
|
|
func (s *Service) ApproveVersion(ctx context.Context, versionID, reviewerNotes string) error {
|
|
return s.db.ApproveVersion(ctx, versionID, reviewerNotes)
|
|
}
|
|
|
|
// RejectVersion rejects a version with feedback
|
|
func (s *Service) RejectVersion(ctx context.Context, versionID string, feedback *RejectionFeedback) error {
|
|
return s.db.RejectVersion(ctx, versionID, feedback.Reason, feedback.Message)
|
|
}
|
|
|
|
// RejectionFeedback contains rejection details
|
|
type RejectionFeedback struct {
|
|
Reason string `json:"reason"`
|
|
Message string `json:"message"`
|
|
Details []RejectionDetail `json:"details,omitempty"`
|
|
CanResubmit bool `json:"can_resubmit"`
|
|
}
|
|
|
|
// RejectionDetail provides specific feedback for an issue
|
|
type RejectionDetail struct {
|
|
File string `json:"file"`
|
|
Line int `json:"line,omitempty"`
|
|
Issue string `json:"issue"`
|
|
Suggestion string `json:"suggestion,omitempty"`
|
|
}
|
|
|
|
// Dangerous patterns to detect in Lua code
|
|
var dangerousPatterns = []struct {
|
|
Pattern *regexp.Regexp
|
|
Reason string
|
|
Severity string
|
|
}{
|
|
{
|
|
regexp.MustCompile(`loadstring\s*\(`),
|
|
"Dynamic code execution via loadstring",
|
|
"critical",
|
|
},
|
|
{
|
|
regexp.MustCompile(`load\s*\([^)]*\)`),
|
|
"Dynamic code loading",
|
|
"critical",
|
|
},
|
|
{
|
|
regexp.MustCompile(`debug\s*\.\s*\w+`),
|
|
"Debug library usage",
|
|
"critical",
|
|
},
|
|
{
|
|
regexp.MustCompile(`os\s*\.\s*execute`),
|
|
"OS command execution",
|
|
"critical",
|
|
},
|
|
{
|
|
regexp.MustCompile(`os\s*\.\s*remove`),
|
|
"File deletion via os.remove",
|
|
"critical",
|
|
},
|
|
{
|
|
regexp.MustCompile(`io\s*\.\s*(open|popen|read|write|lines)`),
|
|
"Direct file I/O operations",
|
|
"critical",
|
|
},
|
|
{
|
|
regexp.MustCompile(`ffi\s*\.\s*\w+`),
|
|
"FFI (foreign function interface) usage",
|
|
"critical",
|
|
},
|
|
{
|
|
regexp.MustCompile(`package\s*\.\s*loadlib`),
|
|
"Native library loading",
|
|
"critical",
|
|
},
|
|
{
|
|
regexp.MustCompile(`package\s*\.\s*cpath`),
|
|
"C library path modification",
|
|
"critical",
|
|
},
|
|
{
|
|
regexp.MustCompile(`rawset\s*\(\s*_G`),
|
|
"Global environment modification",
|
|
"warning",
|
|
},
|
|
{
|
|
regexp.MustCompile(`setfenv\s*\(`),
|
|
"Environment modification",
|
|
"warning",
|
|
},
|
|
{
|
|
regexp.MustCompile(`getfenv\s*\(`),
|
|
"Environment access",
|
|
"warning",
|
|
},
|
|
{
|
|
regexp.MustCompile(`https?://[^\s"']+`),
|
|
"Hardcoded external URL",
|
|
"info",
|
|
},
|
|
{
|
|
regexp.MustCompile(`require\s*\(\s*["'][^"']+["']\s*\)`),
|
|
"Module require (verify allowed modules)",
|
|
"info",
|
|
},
|
|
}
|
|
|
|
func (s *Service) analyzeSecurityRisks(packagePath string) ([]ReviewFlag, error) {
|
|
var flags []ReviewFlag
|
|
|
|
reader, err := zip.OpenReader(packagePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer reader.Close()
|
|
|
|
for _, file := range reader.File {
|
|
if file.FileInfo().IsDir() {
|
|
continue
|
|
}
|
|
|
|
ext := strings.ToLower(strings.TrimPrefix(file.Name, "."))
|
|
if !strings.HasSuffix(file.Name, ".lua") {
|
|
continue
|
|
}
|
|
|
|
// Read Lua file content
|
|
rc, err := file.Open()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
content, err := io.ReadAll(io.LimitReader(rc, 1024*1024)) // 1MB limit
|
|
rc.Close()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
contentStr := string(content)
|
|
|
|
// Check against dangerous patterns
|
|
for _, dp := range dangerousPatterns {
|
|
if dp.Pattern.MatchString(contentStr) {
|
|
// Find line number
|
|
lineNum := findLineNumber(contentStr, dp.Pattern)
|
|
|
|
flags = append(flags, ReviewFlag{
|
|
Type: "SECURITY",
|
|
Severity: dp.Severity,
|
|
Reason: dp.Reason,
|
|
File: file.Name,
|
|
Line: lineNum,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Check for obfuscated code (high entropy, meaningless variable names)
|
|
if isLikelyObfuscated(contentStr) {
|
|
flags = append(flags, ReviewFlag{
|
|
Type: "SECURITY",
|
|
Severity: "warning",
|
|
Reason: "Code appears to be obfuscated",
|
|
File: file.Name,
|
|
})
|
|
}
|
|
|
|
_ = ext // Unused but may be useful for future file type checks
|
|
}
|
|
|
|
return flags, nil
|
|
}
|
|
|
|
func findLineNumber(content string, pattern *regexp.Regexp) int {
|
|
loc := pattern.FindStringIndex(content)
|
|
if loc == nil {
|
|
return 0
|
|
}
|
|
|
|
lineNum := 1
|
|
for i := 0; i < loc[0] && i < len(content); i++ {
|
|
if content[i] == '\n' {
|
|
lineNum++
|
|
}
|
|
}
|
|
return lineNum
|
|
}
|
|
|
|
func isLikelyObfuscated(content string) bool {
|
|
// Simple heuristics for obfuscation detection:
|
|
// 1. High ratio of single-character variable names
|
|
// 2. Many string.char() calls
|
|
// 3. Long lines with minimal whitespace
|
|
|
|
singleCharVars := regexp.MustCompile(`\blocal\s+[a-z]\s*=`)
|
|
matches := singleCharVars.FindAllString(content, -1)
|
|
if len(matches) > 20 {
|
|
return true
|
|
}
|
|
|
|
stringCharCalls := strings.Count(content, "string.char")
|
|
if stringCharCalls > 10 {
|
|
return true
|
|
}
|
|
|
|
// Check for long lines (obfuscators often produce very long lines)
|
|
lines := strings.Split(content, "\n")
|
|
longLines := 0
|
|
for _, line := range lines {
|
|
if len(line) > 500 {
|
|
longLines++
|
|
}
|
|
}
|
|
if longLines > 3 {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (s *Service) checkQuality(manifest *mospkg.Manifest) []ReviewFlag {
|
|
var flags []ReviewFlag
|
|
|
|
if manifest == nil {
|
|
return flags
|
|
}
|
|
|
|
// Check description length
|
|
if len(manifest.Description) < 10 {
|
|
flags = append(flags, ReviewFlag{
|
|
Type: "QUALITY",
|
|
Severity: "info",
|
|
Reason: "Description is very short (less than 10 characters)",
|
|
})
|
|
}
|
|
|
|
// Check for placeholder icon paths
|
|
if manifest.Icons.Size32 == "" && manifest.Icons.Size64 == "" && manifest.Icons.Size128 == "" {
|
|
flags = append(flags, ReviewFlag{
|
|
Type: "QUALITY",
|
|
Severity: "warning",
|
|
Reason: "No icons specified in manifest",
|
|
})
|
|
}
|
|
|
|
// Check for sensitive permissions
|
|
sensitivePerms := map[string]bool{
|
|
"camera": true,
|
|
"microphone": true,
|
|
"contacts": true,
|
|
"location": true,
|
|
}
|
|
for _, perm := range manifest.Permissions {
|
|
if sensitivePerms[perm] {
|
|
flags = append(flags, ReviewFlag{
|
|
Type: "PERMISSION",
|
|
Severity: "info",
|
|
Reason: fmt.Sprintf("App requests sensitive permission: %s", perm),
|
|
})
|
|
}
|
|
}
|
|
|
|
return flags
|
|
}
|
|
|
|
func (s *Service) requiresManualReview(result *FullValidationResult) bool {
|
|
// Always require manual review for:
|
|
// 1. Any critical security flags
|
|
// 2. More than 3 warnings
|
|
// 3. Sensitive permissions
|
|
|
|
criticalCount := 0
|
|
warningCount := 0
|
|
hasSensitivePerms := false
|
|
|
|
for _, flag := range result.Flags {
|
|
switch flag.Severity {
|
|
case "critical":
|
|
criticalCount++
|
|
case "warning":
|
|
warningCount++
|
|
}
|
|
if flag.Type == "PERMISSION" {
|
|
hasSensitivePerms = true
|
|
}
|
|
}
|
|
|
|
if criticalCount > 0 {
|
|
return true
|
|
}
|
|
if warningCount > 3 {
|
|
return true
|
|
}
|
|
if hasSensitivePerms {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// GetReviewQueue returns versions pending review
|
|
func (s *Service) GetReviewQueue(ctx context.Context, limit, offset int) ([]database.VersionWithApp, int, error) {
|
|
return s.db.GetVersionsInReview(ctx, limit, offset)
|
|
}
|
|
|
|
// GetReviewDetails returns details for a specific version under review
|
|
func (s *Service) GetReviewDetails(ctx context.Context, versionID string) (*database.VersionWithApp, error) {
|
|
return s.db.GetVersionWithApp(ctx, versionID)
|
|
}
|