Files
shuffle-cracked/backend/go-app/shuffle-shared/windowsSpecific.go
T
Marat Kharitonov 4f3f07d4dd Crack: bypass license check - force all limits unlimited
- Vendor shuffle-shared v1.2.51 as backend/go-app/shuffle-shared
- Add replace directive in go.mod to use the local moduled copy
- In HandleCheckLicense, force org.Licensed=true and set every
  SyncFeatures limit to 1e9, skipping all license-key logic
- Update Dockerfile to ADD the local shuffle-shared before go build
- Verified: backend image builds successfully via docker
2026-08-12 03:14:24 +03:00

1638 lines
41 KiB
Go

//go:build windows
package shuffle
import (
"strings"
"runtime"
"encoding/json"
"log"
"os"
"bytes"
"time"
"context"
"io"
"errors"
"fmt"
"regexp"
"path/filepath"
"bufio"
"io/fs"
"unsafe" // for pointer control. Not ideal, but ok
"syscall"
"os/exec"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
func scanRegistryUninstall() []Software {
roots := []struct {
key registry.Key
path string
flag uint32
source string
}{
{registry.LOCAL_MACHINE, `Software\Microsoft\Windows\CurrentVersion\Uninstall`, registry.WOW64_64KEY, "registry-lm-64"},
{registry.LOCAL_MACHINE, `Software\Microsoft\Windows\CurrentVersion\Uninstall`, registry.WOW64_32KEY, "registry-lm-32"},
{registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Uninstall`, 0, "registry-cu"},
}
var out []Software
for _, r := range roots {
k, err := registry.OpenKey(r.key, r.path, registry.READ|r.flag)
if err != nil {
continue
}
defer k.Close()
names, _ := k.ReadSubKeyNames(-1)
for _, n := range names {
sk, err := registry.OpenKey(k, n, registry.READ|r.flag)
if err != nil {
continue
}
name, _, _ := sk.GetStringValue("DisplayName")
version, _, _ := sk.GetStringValue("DisplayVersion")
path, _, _ := sk.GetStringValue("InstallLocation")
sk.Close()
if name == "" {
continue
}
out = append(out, Software{
Name: name,
Version: version,
Path: path,
Source: r.source,
})
}
}
return out
}
// Infrastructure package prefixes to drop.
// These are runtime components, not user-installed apps.
var appxSkipPrefixes = []string{
"Microsoft.NET.",
"Microsoft.VCLibs.",
"Microsoft.VCRedist.",
"Microsoft.UI.",
"Microsoft.Windows.",
"Microsoft.Xbox",
"Microsoft.Advertising.",
"Microsoft.Services.",
"Windows.",
"MicrosoftCorporationII.",
}
func scanAppx() []Software {
cmd := `Get-AppxPackage | Select Name, Version | ConvertTo-Json -Compress`
out, err := exec.Command("powershell", "-NoProfile", "-Command", cmd).Output()
if err != nil || len(out) == 0 {
return nil
}
type pkg struct {
Name string
Version string
}
// ConvertTo-Json emits a bare object (not array) when there's exactly
// one result. Try array first, fall back to single object.
var packages []pkg
if err := json.Unmarshal(out, &packages); err != nil {
var single pkg
if err2 := json.Unmarshal(out, &single); err2 != nil {
return nil
}
packages = []pkg{single}
}
var res []Software
for _, p := range packages {
if isInfraAppx(p.Name) {
continue
}
res = append(res, Software{
Name: p.Name,
Version: p.Version,
Source: "appx",
})
}
return res
}
func isInfraAppx(name string) bool {
for _, prefix := range appxSkipPrefixes {
if strings.HasPrefix(name, prefix) {
return true
}
}
return false
}
var roots = []string{
`C:\Program Files`,
`C:\Program Files (x86)`,
}
func scanProgramFiles() []Software {
var out []Software
for _, root := range roots {
filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
// limit depth (cheap heuristic)
if strings.Count(path, string(os.PathSeparator)) > 4 {
return filepath.SkipDir
}
if d.IsDir() {
return nil
}
if !strings.HasSuffix(strings.ToLower(d.Name()), ".exe") {
return nil
}
name, version := getFileVersion(path)
if name == "" {
return nil
}
out = append(out, Software{
Name: name,
Version: version,
Path: path,
Source: "filesystem",
})
return nil
})
}
return out
}
var (
modVersion = windows.NewLazySystemDLL("version.dll")
procGetFileVersionInfo = modVersion.NewProc("GetFileVersionInfoW")
procGetFileVersionSize = modVersion.NewProc("GetFileVersionInfoSizeW")
procVerQueryValue = modVersion.NewProc("VerQueryValueW")
)
func getFileVersion(path string) (name, version string) {
pathPtr, err := windows.UTF16PtrFromString(path)
if err != nil {
return "", ""
}
// First call: get required buffer size
size, _, _ := procGetFileVersionSize.Call(
uintptr(unsafe.Pointer(pathPtr)),
0,
)
if size == 0 {
return "", ""
}
buf := make([]byte, size)
// Second call: fill the buffer
ret, _, _ := procGetFileVersionInfo.Call(
uintptr(unsafe.Pointer(pathPtr)),
0,
size,
uintptr(unsafe.Pointer(&buf[0])),
)
if ret == 0 {
return "", ""
}
// Query the translation table to find the right language/codepage pair
type langCodepage struct{ lang, codepage uint16 }
var translations *langCodepage
var transLen uint32
ret, _, _ = procVerQueryValue.Call(
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(`\VarFileInfo\Translation`))),
uintptr(unsafe.Pointer(&translations)),
uintptr(unsafe.Pointer(&transLen)),
)
if ret == 0 || transLen == 0 {
return "", ""
}
// Use the first available translation
lang := fmt.Sprintf(`\StringFileInfo\%04x%04x\`, translations.lang, translations.codepage)
name = queryStringValue(buf, lang+"ProductName")
version = queryStringValue(buf, lang+"ProductVersion")
return name, version
}
func queryStringValue(buf []byte, key string) string {
keyPtr, err := windows.UTF16PtrFromString(key)
if err != nil {
return ""
}
var valPtr uintptr
var valLen uint32
ret, _, _ := procVerQueryValue.Call(
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(keyPtr)),
uintptr(unsafe.Pointer(&valPtr)),
uintptr(unsafe.Pointer(&valLen)),
)
if ret == 0 || valLen == 0 {
return ""
}
// valPtr points into buf, valLen is in characters (UTF-16)
utf16Slice := unsafe.Slice((*uint16)(unsafe.Pointer(valPtr)), valLen)
return windows.UTF16ToString(utf16Slice)
}
func scanWinget() []Software {
out, err := exec.Command(
"winget", "list",
"--disable-interactivity",
"--accept-source-agreements",
).Output()
if err != nil || len(out) == 0 {
return nil
}
lines := strings.Split(string(out), "\n")
// Find the header line — it contains "Name" and "Id"
headerIdx := -1
for i, l := range lines {
if strings.Contains(l, "Name") && strings.Contains(l, "Id") {
headerIdx = i
break
}
}
if headerIdx < 0 || headerIdx+2 >= len(lines) {
return nil
}
header := lines[headerIdx]
// Column start positions by header label
nameCol := strings.Index(header, "Name")
idCol := strings.Index(header, "Id")
versionCol := strings.Index(header, "Version")
sourceCol := strings.Index(header, "Source") // may be -1
if nameCol < 0 || idCol < 0 || versionCol < 0 {
return nil
}
// Skip header + separator line (headerIdx+1 is "----")
var res []Software
for _, line := range lines[headerIdx+2:] {
// Trim Windows line endings; skip short/empty lines
line = strings.TrimRight(line, "\r")
if len(line) < versionCol+1 {
continue
}
name := columnSlice(line, nameCol, idCol)
version := columnSlice(line, versionCol, sourceCol)
if name == "" {
continue
}
res = append(res, Software{
Name: name,
Version: version,
Source: "winget",
})
}
return res
}
// columnSlice extracts text between start and end column positions,
// trimming whitespace. If end is -1 (column not present), reads to EOL.
func columnSlice(line string, start, end int) string {
if start >= len(line) {
return ""
}
if end < 0 || end >= len(line) {
return strings.TrimSpace(line[start:])
}
return strings.TrimSpace(line[start:end])
}
func dedupe(in []Software) []Software {
seen := map[string]bool{}
var out []Software
for _, s := range in {
key := strings.ToLower(s.Name + "|" + s.Version)
if seen[key] {
continue
}
seen[key] = true
out = append(out, s)
}
return out
}
func ListInstalledSoftware() []Software {
var all []Software
all = append(all, scanRegistryUninstall()...)
all = append(all, scanAppx()...)
all = append(all, scanProgramFiles()...)
all = append(all, scanWinget()...)
return dedupe(all)
}
func IsElevated() bool {
var token windows.Token
err := windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_QUERY, &token)
if err != nil {
return false
}
defer token.Close()
return token.IsElevated()
}
func extractRegValue(output string) string {
// Windows reg output format:
// " ValueName REG_TYPE ActualValue"
// We need to extract "ActualValue"
lines := strings.Split(output, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
// Skip empty lines and the key path line
if line == "" || strings.HasPrefix(line, "HKEY_") {
continue
}
// Split by whitespace and get the last non-empty field
fields := strings.Fields(line)
if len(fields) >= 3 {
// Last field is the value
return fields[len(fields)-1]
}
}
return ""
}
func isEncryptedWindows() bool {
out, err := exec.Command("manage-bde", "-status", "C:").Output()
if err != nil {
return false
}
s := strings.ToLower(string(out))
// key signals
return strings.Contains(s, "protection on")
}
func IsDiskEncrypted() bool {
switch runtime.GOOS {
case "windows":
return isEncryptedWindows()
default:
return false
}
}
func GetProfiler() string {
cmds := []string{
"(Get-CimInstance Win32_BIOS).SerialNumber",
"(Get-CimInstance Win32_ComputerSystemProduct).IdentifyingNumber",
}
for _, c := range cmds {
out, err := exec.Command("powershell", "-Command", c).Output()
if err == nil {
s := strings.TrimSpace(string(out))
if isValidSerial(s) {
return s
}
}
}
return "failed to get profiler"
}
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procCreateJobObjectW = kernel32.NewProc("CreateJobObjectW")
procAssignProcessToJobObject = kernel32.NewProc("AssignProcessToJobObject")
procTerminateJobObject = kernel32.NewProc("TerminateJobObject")
)
func createJobObject() (syscall.Handle, error) {
r1, _, err := procCreateJobObjectW.Call(0, 0)
if r1 == 0 {
return 0, err
}
return syscall.Handle(r1), nil
}
func assignProcessToJob(job syscall.Handle, p *os.Process) error {
r1, _, err := procAssignProcessToJobObject.Call(
uintptr(job),
uintptr(p.Pid),
)
if r1 == 0 {
return err
}
return nil
}
func RunCommandString(command string, timeout time.Duration, onStream StreamFn) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
cmd := exec.CommandContext(ctx, "cmd", "/C", command)
stdout, err := cmd.StdoutPipe()
if err != nil {
return "", err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return "", err
}
if err := cmd.Start(); err != nil {
return "", err
}
var out bytes.Buffer
read := func(r io.ReadCloser) {
buf := make([]byte, 32*1024)
for {
n, err := r.Read(buf)
if n > 0 {
chunk := buf[:n]
out.Write(chunk)
if onStream != nil {
onStream(string(chunk))
}
}
if err != nil {
return
}
}
}
go read(stdout)
go read(stderr)
waitCh := make(chan error, 1)
go func() {
waitCh <- cmd.Wait()
}()
select {
case err := <-waitCh:
return out.String(), err
case <-ctx.Done():
// timeout path: kill only the parent process
_ = cmd.Process.Kill()
<-waitCh // ensure cleanup
return out.String(), fmt.Errorf("timeout after %s", timeout)
}
}
func (c *AuditLogCollector) Stop() {
return
}
func (c *AuditLogCollector) LogCollectorStart(ctx context.Context) error {
return errors.New("Not implemented on windows")
}
func NewAuditLogCollector(config TelemetryConfig) (*AuditLogCollector, error) {
auditLogCollector := AuditLogCollector{}
return &auditLogCollector, errors.New("Not implemented on windows")
}
func queryRegValue(name string) (string, error) {
cmd := exec.Command(
"reg", "query",
`HKEY_CURRENT_USER\Control Panel\Desktop`,
"/v", name,
)
out, err := cmd.CombinedOutput()
if err != nil {
return "", err
}
lines := strings.Split(string(out), "\n")
for _, line := range lines {
if strings.Contains(line, name) {
fields := strings.Fields(line)
if len(fields) >= 3 {
return fields[len(fields)-1], nil
}
}
}
return "", fmt.Errorf("value not found")
}
func IsAutomaticScreenlockEnabled() bool {
activeStr, err := queryRegValue("ScreenSaveActive")
if err != nil {
log.Printf("[ERROR] ScreenSaveActive: %v", err)
return false
}
secureStr, err := queryRegValue("ScreenSaverIsSecure")
if err != nil {
log.Printf("[ERROR] ScreenSaverIsSecure: %v", err)
return false
}
timeoutStr, err := queryRegValue("ScreenSaveTimeOut")
if err != nil {
log.Printf("[ERROR] ScreenSaveTimeOut: %v", err)
return false
}
active := parseInt(activeStr)
secure := parseInt(secureStr)
timeout := parseInt(timeoutStr)
return active == 1 && secure == 1 && timeout <= 900
}
type osVersionInfoEx struct {
dwOSVersionInfoSize uint32
dwMajorVersion uint32
dwMinorVersion uint32
dwBuildNumber uint32
dwPlatformId uint32
szCSDVersion [128]uint16
wServicePackMajor uint16
wServicePackMinor uint16
wSuiteMask uint16
wProductType byte
wReserved byte
}
const (
backupFile = "C:\\Windows\\Temp\\firewall_backup_edr.wfw"
)
func isAdmin() bool {
_, err := os.Open("\\\\.\\PHYSICALDRIVE0")
return err == nil
}
func isolateHostWindows(allowIPs []string) error {
// Must run as admin
if !isAdmin() {
return fmt.Errorf("requires administrator privileges")
}
// 1. Backup firewall state
exec.Command("netsh", "advfirewall", "export", backupFile).Run()
// 2. Set default block policies
cmds := [][]string{
{"netsh", "advfirewall", "set", "allprofiles", "firewallpolicy", "blockinbound,blockoutbound"},
}
for _, c := range cmds {
if err := exec.Command(c[0], c[1:]...).Run(); err != nil {
return fmt.Errorf("failed to set firewall policy: %w", err)
}
}
// 3. Allow loopback explicitly
exec.Command("netsh", "advfirewall", "firewall", "add", "rule",
"name=EDR-Allow-Loopback",
"dir=in",
"action=allow",
"interface=any",
"enable=yes").Run()
exec.Command("netsh", "advfirewall", "firewall", "add", "rule",
"name=EDR-Allow-Loopback-Out",
"dir=out",
"action=allow",
"interface=any",
"enable=yes").Run()
// 4. Allow EDR endpoints
for _, ip := range allowIPs {
exec.Command("netsh", "advfirewall", "firewall", "add", "rule",
fmt.Sprintf("name=EDR-Allow-%s", ip),
"dir=out",
"action=allow",
fmt.Sprintf("remoteip=%s", ip),
"enable=yes").Run()
exec.Command("netsh", "advfirewall", "firewall", "add", "rule",
fmt.Sprintf("name=EDR-Allow-In-%s", ip),
"dir=in",
"action=allow",
fmt.Sprintf("remoteip=%s", ip),
"enable=yes").Run()
}
return nil
}
func unisolateHostWindows() error {
if !isAdmin() {
return fmt.Errorf("requires administrator privileges")
}
// Restore firewall config
return exec.Command("netsh", "advfirewall", "import", backupFile).Run()
}
func isolateHost(allowIPs []string) error {
return isolateHostWindows(allowIPs)
}
func unisolateHost() error {
return unisolateHostWindows()
}
// ── Constructor ──────────────────────────────────────────────────────────────
func NewScanner() *Scanner {
return &Scanner{
results: make(chan ProjectInfo),
visited: make(map[string]bool),
}
}
// ── Public entry point ───────────────────────────────────────────────────────
func (s *Scanner) Scan(rootDir string) ([]ProjectInfo, error) {
absRoot, err := filepath.Abs(rootDir)
if err != nil {
return nil, fmt.Errorf("invalid root directory: %w", err)
}
s.wg.Add(1)
go s.scanDir(absRoot)
var results []ProjectInfo
done := make(chan struct{})
go func() {
for p := range s.results {
results = append(results, p)
}
close(done)
}()
s.wg.Wait()
close(s.results)
<-done
return results, nil
}
// ── Directory walker ─────────────────────────────────────────────────────────
func (s *Scanner) scanDir(dir string) {
defer s.wg.Done()
// Resolve symlinks so we never visit the same inode twice.
real, err := filepath.EvalSymlinks(dir)
if err != nil {
return
}
s.mu.Lock()
if s.visited[real] {
s.mu.Unlock()
return
}
s.visited[real] = true
s.mu.Unlock()
entries, err := os.ReadDir(dir)
if err != nil {
return
}
for _, entry := range entries {
if shouldSkip(entry.Name()) {
continue
}
fullPath := filepath.Join(dir, entry.Name())
if !entry.IsDir() {
continue
}
if projectType := detectProjectType(fullPath); projectType != "" {
packages := extractPackages(fullPath, projectType)
s.results <- ProjectInfo{
Path: fullPath,
Type: projectType,
Packages: packages,
}
// Do not recurse into found projects — avoids duplicates.
continue
}
s.wg.Add(1)
go s.scanDir(fullPath)
}
}
// ── Skip list ────────────────────────────────────────────────────────────────
// skipDirs is the unified skip list for all platforms.
// Windows-specific entries are appended at init time.
var skipDirs = map[string]bool{
// VCS
".git": true,
".hg": true,
".svn": true,
// Dependency caches
"node_modules": true,
"vendor": true,
".venv": true,
"venv": true,
".env": true,
// IDE / tooling
".vscode": true,
".idea": true,
// Build output
"dist": true,
"build": true,
"target": true,
"out": true,
"bin": true,
"obj": true, // .NET
// Caches
".cache": true,
"__pycache__": true,
}
func init() {
if runtime.GOOS == "windows" {
// Windows system and user-profile noise — these directories sit under
// %USERPROFILE% but contain no user code.
for _, d := range []string{
"AppData",
"Application Data",
"Local Settings",
"MicrosoftEdgeBackups",
"OneDrive", // mirror of cloud files, not local projects
"Windows",
"Program Files",
"Program Files (x86)",
"ProgramData",
"$Recycle.Bin",
"System Volume Information",
"Recovery",
} {
skipDirs[d] = true
}
}
}
func shouldSkip(name string) bool {
if skipDirs[name] {
return true
}
// Hidden directories (dot-prefixed) on Unix; also catches .git etc. on Windows.
if strings.HasPrefix(name, ".") && name != "." {
return true
}
return false
}
// ── Project detection ────────────────────────────────────────────────────────
func detectProjectType(dir string) string {
if fileExists(filepath.Join(dir, "go.mod")) {
return "golang"
}
if fileExists(filepath.Join(dir, "pyproject.toml")) ||
fileExists(filepath.Join(dir, "requirements.txt")) ||
fileExists(filepath.Join(dir, "Pipfile")) {
return "python"
}
if fileExists(filepath.Join(dir, "package.json")) {
return "javascript"
}
if fileExists(filepath.Join(dir, "pom.xml")) ||
fileExists(filepath.Join(dir, "build.gradle")) ||
fileExists(filepath.Join(dir, "build.gradle.kts")) {
return "java"
}
if fileExists(filepath.Join(dir, "Gemfile")) ||
fileExists(filepath.Join(dir, "Rakefile")) {
return "ruby"
}
// .NET: must ReadDir — glob patterns are not valid os.Stat paths.
if entries, err := os.ReadDir(dir); err == nil {
for _, e := range entries {
n := e.Name()
if strings.HasSuffix(n, ".csproj") ||
strings.HasSuffix(n, ".vbproj") ||
strings.HasSuffix(n, ".fsproj") {
return "dotnet"
}
}
}
return ""
}
// ── Dispatcher ───────────────────────────────────────────────────────────────
func extractPackages(dir, projectType string) []Software {
switch projectType {
case "golang":
return extractGoPackages(dir)
case "python":
return extractPythonPackages(dir)
case "javascript":
return extractJavaScriptPackages(dir)
case "java":
return extractJavaPackages(dir)
case "ruby":
return extractRubyPackages(dir)
case "dotnet":
return extractDotnetPackages(dir)
}
return nil
}
// ── Go ───────────────────────────────────────────────────────────────────────
func extractGoPackages(dir string) []Software {
f, err := os.Open(filepath.Join(dir, "go.mod"))
if err != nil {
return nil
}
defer f.Close()
var pkgs []Software
sc := bufio.NewScanner(f)
inBlock := false
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
switch {
case line == "require (":
inBlock = true
case line == ")" && inBlock:
inBlock = false
case strings.HasPrefix(line, "require ") && !inBlock:
// Single-line form: require github.com/foo/bar v1.2.3
parts := strings.Fields(line)
if len(parts) == 3 {
pkgs = append(pkgs, Software{Name: parts[1], Version: parts[2]})
}
case inBlock && line != "" && !strings.HasPrefix(line, "//"):
parts := strings.Fields(line)
if len(parts) >= 2 {
pkgs = append(pkgs, Software{Name: parts[0], Version: parts[1]})
} else if len(parts) == 1 {
pkgs = append(pkgs, Software{Name: parts[0]})
}
}
}
return pkgs
}
// ── Python ───────────────────────────────────────────────────────────────────
func extractPythonPackages(dir string) []Software {
if data, err := os.ReadFile(filepath.Join(dir, "pyproject.toml")); err == nil {
if pkgs := parsePyprojectToml(string(data)); len(pkgs) > 0 {
return pkgs
}
}
if data, err := os.ReadFile(filepath.Join(dir, "requirements.txt")); err == nil {
if pkgs := parseRequirementsTxt(string(data)); len(pkgs) > 0 {
return pkgs
}
}
if data, err := os.ReadFile(filepath.Join(dir, "Pipfile")); err == nil {
return parsePipfile(string(data))
}
return nil
}
// versionOps are Python version specifier operators, longest-match first.
var versionOps = []string{">=", "<=", "==", "~=", "!=", ">", "<", ";"}
func splitPyDep(dep string) (name, version string) {
minIdx := len(dep)
for _, op := range versionOps {
if idx := strings.Index(dep, op); idx >= 0 && idx < minIdx {
minIdx = idx
}
}
if minIdx < len(dep) {
return strings.TrimSpace(dep[:minIdx]), strings.TrimSpace(dep[minIdx:])
}
return strings.TrimSpace(dep), ""
}
func parseRequirementsTxt(content string) []Software {
var pkgs []Software
sc := bufio.NewScanner(strings.NewReader(content))
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "-") {
continue
}
// Strip inline comments.
if i := strings.Index(line, " #"); i >= 0 {
line = strings.TrimSpace(line[:i])
}
name, version := splitPyDep(line)
if name != "" {
pkgs = append(pkgs, Software{Name: name, Version: version})
}
}
return pkgs
}
func parsePyprojectToml(content string) []Software {
var pkgs []Software
inDeps := false
sc := bufio.NewScanner(strings.NewReader(content))
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if strings.Contains(line, "[tool.poetry.dependencies]") ||
strings.Contains(line, "[project]") && strings.Contains(line, "requires") {
inDeps = true
continue
}
// Any new section ends deps block.
if inDeps && strings.HasPrefix(line, "[") {
inDeps = false
}
// Array-style: "django>=3.0"
if inDeps && strings.HasPrefix(line, `"`) {
raw := strings.Trim(line, `",`)
name, version := splitPyDep(raw)
if name != "" {
pkgs = append(pkgs, Software{Name: name, Version: version})
}
}
// TOML key = "version" style: django = ">=3.0"
if inDeps && strings.Contains(line, "=") && !strings.HasPrefix(line, "[") {
parts := strings.SplitN(line, "=", 2)
name := strings.TrimSpace(parts[0])
version := strings.Trim(strings.TrimSpace(parts[1]), `"'`)
if name != "" && name != "python" {
pkgs = append(pkgs, Software{Name: name, Version: version})
}
}
}
return pkgs
}
func parsePipfile(content string) []Software {
var pkgs []Software
inPackages := false
sc := bufio.NewScanner(strings.NewReader(content))
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "[packages]" || line == "[dev-packages]" {
inPackages = true
continue
}
if inPackages && strings.HasPrefix(line, "[") {
inPackages = false
}
if inPackages && line != "" && strings.Contains(line, "=") {
parts := strings.SplitN(line, "=", 2)
name := strings.TrimSpace(parts[0])
version := strings.Trim(strings.TrimSpace(parts[1]), `"'`)
if name != "" {
pkgs = append(pkgs, Software{Name: name, Version: version})
}
}
}
return pkgs
}
// ── JavaScript / TypeScript ──────────────────────────────────────────────────
func extractJavaScriptPackages(dir string) []Software {
data, err := os.ReadFile(filepath.Join(dir, "package.json"))
if err != nil {
return nil
}
var pkg struct {
Dependencies map[string]string `json:"dependencies"`
DevDependencies map[string]string `json:"devDependencies"`
}
if err := json.Unmarshal(data, &pkg); err != nil {
return nil
}
var pkgs []Software
for n, v := range pkg.Dependencies {
pkgs = append(pkgs, Software{Name: n, Version: v})
}
for n, v := range pkg.DevDependencies {
pkgs = append(pkgs, Software{Name: n, Version: v})
}
return pkgs
}
// ── Java ─────────────────────────────────────────────────────────────────────
func extractJavaPackages(dir string) []Software {
if data, err := os.ReadFile(filepath.Join(dir, "pom.xml")); err == nil {
return parsePomXml(string(data))
}
if data, err := os.ReadFile(filepath.Join(dir, "build.gradle")); err == nil {
return parseGradleBuild(string(data))
}
if data, err := os.ReadFile(filepath.Join(dir, "build.gradle.kts")); err == nil {
return parseGradleBuild(string(data))
}
return nil
}
// parsePomXml collects groupId:artifactId pairs from Maven pom.xml.
// The original code only collected groupId, producing half-names like "org.springframework".
func parsePomXml(content string) []Software {
var pkgs []Software
inDeps := false
var groupID, artifactID string
sc := bufio.NewScanner(strings.NewReader(content))
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if strings.Contains(line, "<dependencies>") {
inDeps = true
continue
}
if strings.Contains(line, "</dependencies>") {
inDeps = false
groupID, artifactID = "", ""
continue
}
if strings.Contains(line, "</dependency>") {
groupID, artifactID = "", ""
continue
}
if !inDeps {
continue
}
if groupID == "" {
if v := extractXmlValue(line, "groupId"); v != "" {
groupID = v
}
}
if artifactID == "" {
if v := extractXmlValue(line, "artifactId"); v != "" {
artifactID = v
}
}
if groupID != "" && artifactID != "" {
version := extractXmlValue(line, "version")
pkgs = append(pkgs, Software{
Name: groupID + ":" + artifactID,
Version: version,
})
groupID, artifactID = "", ""
}
}
return pkgs
}
// gradleDepRe matches both groovy and Kotlin DSL dependency strings:
//
// implementation 'group:artifact:version'
// implementation("group:artifact:version")
var gradleDepRe = regexp.MustCompile(`(?:implementation|compile|api|testImplementation|runtimeOnly)\s*[\("']([^"']+)[\("']`)
func parseGradleBuild(content string) []Software {
var pkgs []Software
for _, match := range gradleDepRe.FindAllStringSubmatch(content, -1) {
dep := match[1]
parts := strings.Split(dep, ":")
switch len(parts) {
case 3:
pkgs = append(pkgs, Software{Name: parts[0] + ":" + parts[1], Version: parts[2]})
case 2:
pkgs = append(pkgs, Software{Name: parts[0], Version: parts[1]})
}
}
return pkgs
}
// ── Ruby ─────────────────────────────────────────────────────────────────────
// gemRe matches lines like:
//
// gem 'rails', '~> 7.0'
// gem "devise", ">= 4.0"
// gem 'puma'
var gemRe = regexp.MustCompile(`^\s*gem\s+['"]([^'"]+)['"](?:\s*,\s*['"]([^'"]+)['"])?`)
func extractRubyPackages(dir string) []Software {
data, err := os.ReadFile(filepath.Join(dir, "Gemfile"))
if err != nil {
return nil
}
return parseGemfile(string(data))
}
func parseGemfile(content string) []Software {
var pkgs []Software
sc := bufio.NewScanner(strings.NewReader(content))
for sc.Scan() {
line := sc.Text()
if m := gemRe.FindStringSubmatch(line); m != nil {
pkgs = append(pkgs, Software{Name: m[1], Version: m[2]})
}
}
return pkgs
}
// ── .NET ─────────────────────────────────────────────────────────────────────
func extractDotnetPackages(dir string) []Software {
entries, err := os.ReadDir(dir)
if err != nil {
return nil
}
for _, e := range entries {
n := e.Name()
if strings.HasSuffix(n, ".csproj") ||
strings.HasSuffix(n, ".vbproj") ||
strings.HasSuffix(n, ".fsproj") {
data, err := os.ReadFile(filepath.Join(dir, n))
if err != nil {
continue
}
return parseDotnetProjectFile(string(data))
}
}
return nil
}
func parseDotnetProjectFile(content string) []Software {
var pkgs []Software
sc := bufio.NewScanner(strings.NewReader(content))
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if !strings.Contains(line, "PackageReference") {
continue
}
name := extractXmlAttr(line, "Include")
version := extractXmlAttr(line, "Version")
if name != "" {
pkgs = append(pkgs, Software{Name: name, Version: version})
}
}
return pkgs
}
// ── XML helpers ──────────────────────────────────────────────────────────────
// extractXmlValue extracts a simple tag value: <tag>value</tag>
func extractXmlValue(line, tag string) string {
open := "<" + tag + ">"
close := "</" + tag + ">"
s := strings.Index(line, open)
e := strings.Index(line, close)
if s >= 0 && e > s {
return line[s+len(open) : e]
}
return ""
}
// extractXmlAttr extracts an XML attribute value: attr="value"
func extractXmlAttr(line, attr string) string {
needle := attr + `="`
s := strings.Index(line, needle)
if s < 0 {
return ""
}
s += len(needle)
e := strings.Index(line[s:], `"`)
if e < 0 {
return ""
}
return line[s : s+e]
}
// ── Public API ───────────────────────────────────────────────────────────────
// goModCacheDir returns the OS-appropriate Go module cache path fragment
// so we can filter it regardless of platform.
func goModCacheDir() string {
// GOPATH may be set explicitly; fall back to the default ~/go.
gopath := os.Getenv("GOPATH")
if gopath == "" {
home, _ := os.UserHomeDir()
gopath = filepath.Join(home, "go")
}
return filepath.Join(gopath, "pkg", "mod")
}
//func ListCodeScannerProjects() []ProjectInfo {
// log.Printf("[WARNING] Codescanner not implemented on windows yet.")
//
// return []ProjectInfo{}
//}
func ListCodeScannerProjects() []ProjectInfo {
homeDir, err := os.UserHomeDir()
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting home directory: %v\n", err)
return nil
}
modCache := goModCacheDir()
sc := NewScanner()
projects, err := sc.Scan(homeDir)
if err != nil {
log.Printf("[ERROR] Problem in codescanner: %v\n", err)
}
var out []ProjectInfo
for _, p := range projects {
if p.Path == "" || len(p.Packages) == 0 {
continue
}
// Skip the Go module download cache — these are vendored copies,
// not the user's own projects.
if strings.HasPrefix(p.Path, modCache) {
continue
}
out = append(out, p)
}
return out
}
// GetDisplaySizeWindows returns the dimensions of every active display.
// Prefer calling Screenshot() if you need both image and size — it is cheaper.
func GetDisplaySizeWindows() ([]DisplaySize, error) {
if err := checkInteractiveSession(); err != nil {
return nil, err
}
out, err := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", `
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Screen]::AllScreens |
Select-Object @{N='Width';E={$_.Bounds.Width}}, @{N='Height';E={$_.Bounds.Height}} |
ConvertTo-Json -Compress
`).Output()
if err != nil {
return nil, fmt.Errorf("querying displays: %w", err)
}
trimmed := strings.TrimSpace(string(out))
if strings.HasPrefix(trimmed, "{") {
trimmed = "[" + trimmed + "]"
}
var records []struct {
Width int `json:"Width"`
Height int `json:"Height"`
}
if err := json.Unmarshal([]byte(trimmed), &records); err != nil {
return nil, fmt.Errorf("parsing display sizes: %w", err)
}
sizes := make([]DisplaySize, len(records))
for i, r := range records {
sizes[i] = DisplaySize{DisplayID: i + 1, Width: r.Width, Height: r.Height}
}
return sizes, nil
}
// GetCursorPositionWindows returns the current cursor position.
// Origin (0,0) is the top-left of the primary display.
// Prefer calling Screenshot() if you need both image and cursor — it is cheaper.
func GetCursorPositionWindows() (Position, error) {
if err := checkInteractiveSession(); err != nil {
return Position{}, err
}
out, err := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", `
Add-Type -AssemblyName System.Windows.Forms
$p = [System.Windows.Forms.Cursor]::Position
Write-Output "$($p.X) $($p.Y)"`).Output()
if err != nil {
return Position{}, fmt.Errorf("querying cursor position: %w", err)
}
var x, y float64
if _, err := fmt.Sscanf(strings.TrimSpace(string(out)), "%f %f", &x, &y); err != nil {
return Position{}, fmt.Errorf("parsing cursor position %q: %w", out, err)
}
return Position{X: x, Y: y}, nil
}
// checkInteractiveSession returns an error if the process is running in
// Windows Session 0 (the non-interactive service session). Session 0 has no
// display, no cursor, and no desktop — all GUI calls will fail or hang there.
func checkInteractiveSession() error {
out, err := exec.Command(
"powershell", "-NoProfile", "-NonInteractive", "-Command",
`(Get-Process -Id $PID).SessionId`,
).Output()
if err != nil {
// Can't determine session — proceed and let the caller handle failure.
return nil
}
var id int
if _, err := fmt.Sscanf(strings.TrimSpace(string(out)), "%d", &id); err != nil {
return nil
}
if id == 0 {
return fmt.Errorf("running in Session 0 (non-interactive service session) — no display available")
}
return nil
}
// ========================
// Windows API bindings
// ========================
var (
user32 = windows.NewLazySystemDLL("user32.dll")
procSetCursorPos = user32.NewProc("SetCursorPos")
procMouseEvent = user32.NewProc("mouse_event")
procKeybdEvent = user32.NewProc("keybd_event")
)
// ========================
// Mouse constants
// ========================
const (
MOUSE_LEFTDOWN = 0x0002
MOUSE_LEFTUP = 0x0004
MOUSE_RIGHTDOWN = 0x0008
MOUSE_RIGHTUP = 0x0010
)
// ========================
// RemoteControl methods
// ========================
func remoteControlBatch(batch RemoteControlActionBatch) error {
for _, a := range batch.Actions {
remoteControlExecute(a)
}
return nil
}
func remoteControlExecute(a RemoteControl) {
switch a.Op {
// -------- Mouse --------
case "mouse.move":
x := getInt(a.Params, "x")
y := getInt(a.Params, "y")
setCursor(x, y)
case "mouse.click":
x := getInt(a.Params, "x")
y := getInt(a.Params, "y")
button := getString(a.Params, "button")
delay := getInt(a.Params, "delay_ms")
setCursor(x, y)
time.Sleep(time.Duration(delay) * time.Millisecond)
mouseDown(button)
time.Sleep(50 * time.Millisecond)
mouseUp(button)
case "mouse.drag":
fx := getInt(a.Params, "from_x")
fy := getInt(a.Params, "from_y")
tx := getInt(a.Params, "to_x")
ty := getInt(a.Params, "to_y")
button := getString(a.Params, "button")
setCursor(fx, fy)
time.Sleep(50 * time.Millisecond)
mouseDown(button)
time.Sleep(50 * time.Millisecond)
setCursor(tx, ty)
time.Sleep(50 * time.Millisecond)
mouseUp(button)
// -------- Keyboard --------
case "keyboard.press":
key := getInt(a.Params, "key")
keyPress(uint16(key))
// -------- Utility --------
case "system.wait":
ms := getInt(a.Params, "ms")
time.Sleep(time.Duration(ms) * time.Millisecond)
}
}
// ========================
// Windows input functions
// ========================
func setCursor(x, y int) {
procSetCursorPos.Call(uintptr(x), uintptr(y))
}
func mouseDown(button string) {
if button == "right" {
procMouseEvent.Call(MOUSE_RIGHTDOWN, 0, 0, 0, 0)
return
}
procMouseEvent.Call(MOUSE_LEFTDOWN, 0, 0, 0, 0)
}
func mouseUp(button string) {
if button == "right" {
procMouseEvent.Call(MOUSE_RIGHTUP, 0, 0, 0, 0)
return
}
procMouseEvent.Call(MOUSE_LEFTUP, 0, 0, 0, 0)
}
func keyPress(vk uint16) {
procKeybdEvent.Call(uintptr(vk), 0, 0, 0)
time.Sleep(30 * time.Millisecond)
procKeybdEvent.Call(uintptr(vk), 0, 2, 0)
}
// ========================
// Helpers
// ========================
func getInt(m map[string]any, key string) int {
if v, ok := m[key]; ok {
switch t := v.(type) {
case float64:
return int(t)
case int:
return t
}
}
return 0
}
func getString(m map[string]any, key string) string {
if v, ok := m[key]; ok {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
func Screenshot() ([]ScreenshotWrapper, error) {
if err := checkInteractiveSession(); err != nil {
return nil, err
}
// We need per-display images, so we capture each screen individually
// and collect metadata for all of them in one PowerShell call.
tmpDir := os.TempDir()
ts := time.Now().UnixNano()
// The script does three things:
// 1. Captures each Screen individually to a temp file named by index.
// 2. Reads cursor position.
// 3. Emits a JSON array describing each screen (dimensions, cursor,
// temp file path) so Go can read it back without more parsing.
script := fmt.Sprintf(`
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$cursor = [System.Windows.Forms.Cursor]::Position
$screens = [System.Windows.Forms.Screen]::AllScreens
$results = @()
for ($i = 0; $i -lt $screens.Length; $i++) {
$s = $screens[$i]
$path = "%s\edr-%d-d$i.png"
$bmp = New-Object System.Drawing.Bitmap($s.Bounds.Width, $s.Bounds.Height)
$gfx = [System.Drawing.Graphics]::FromImage($bmp)
$gfx.CopyFromScreen($s.Bounds.Left, $s.Bounds.Top, 0, 0, $bmp.Size)
$bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png)
$gfx.Dispose()
$bmp.Dispose()
$results += [PSCustomObject]@{
Path = $path
Width = $s.Bounds.Width
Height = $s.Bounds.Height
CursorX = $cursor.X
CursorY = $cursor.Y
}
}
$results | ConvertTo-Json -Compress
`, tmpDir, ts)
command := exec.Command(
"powershell", "-WindowStyle", "Hidden", "-NoProfile", "-NonInteractive", "-Command", script,
)
command.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x08000000, // CREATE_NO_WINDOW
}
out, err := command.Output()
if err != nil {
return nil, fmt.Errorf("screenshot script failed: %w", err)
}
// PowerShell emits a bare object (not array) when there is exactly one
// screen. Normalise to array so json.Unmarshal always gets a slice.
trimmed := strings.TrimSpace(string(out))
if strings.HasPrefix(trimmed, "{") {
trimmed = "[" + trimmed + "]"
}
var records []struct {
Path string `json:"Path"`
Width int `json:"Width"`
Height int `json:"Height"`
CursorX float64 `json:"CursorX"`
CursorY float64 `json:"CursorY"`
}
if err := json.Unmarshal([]byte(trimmed), &records); err != nil {
return nil, fmt.Errorf("parsing screenshot metadata: %w", err)
}
wrappers := make([]ScreenshotWrapper, 0, len(records))
for i, r := range records {
data, err := os.ReadFile(r.Path)
os.Remove(r.Path) // clean up regardless of read outcome
if err != nil {
return nil, fmt.Errorf("reading screenshot for display %d: %w", i, err)
}
wrappers = append(wrappers, ScreenshotWrapper{
Image: data,
ScreenSize: DisplaySize{DisplayID: i + 1, Width: r.Width, Height: r.Height},
Cursor: Position{X: r.CursorX, Y: r.CursorY},
})
}
return wrappers, nil
}