mirror of
https://github.com/safing/portbase
synced 2025-09-01 18:19:57 +00:00
Merge pull request #102 from safing/feature/binary-metadata
Add binary metadata getters for windows
This commit is contained in:
commit
ab21e88ae9
7 changed files with 321 additions and 0 deletions
86
utils/osdetail/binmeta.go
Normal file
86
utils/osdetail/binmeta.go
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
package osdetail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
segmentsSplitter = regexp.MustCompile("[^A-Za-z0-9]*[A-Z]?[a-z0-9]*")
|
||||||
|
nameOnly = regexp.MustCompile("^[A-Za-z0-9]+$")
|
||||||
|
delimiters = regexp.MustCompile("^[^A-Za-z0-9]+")
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateBinaryNameFromPath generates a more human readable binary name from
|
||||||
|
// the given path. This function is used as fallback in the GetBinaryName
|
||||||
|
// functions.
|
||||||
|
func GenerateBinaryNameFromPath(path string) string {
|
||||||
|
// Get file name from path.
|
||||||
|
_, fileName := filepath.Split(path)
|
||||||
|
|
||||||
|
// Split up into segments.
|
||||||
|
segments := segmentsSplitter.FindAllString(fileName, -1)
|
||||||
|
|
||||||
|
// Remove last segment if it's an extension.
|
||||||
|
if len(segments) >= 2 &&
|
||||||
|
strings.HasPrefix(segments[len(segments)-1], ".") {
|
||||||
|
segments = segments[:len(segments)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go through segments and collect name parts.
|
||||||
|
nameParts := make([]string, 0, len(segments))
|
||||||
|
var fragments string
|
||||||
|
for _, segment := range segments {
|
||||||
|
// Group very short segments.
|
||||||
|
if len(segment) <= 3 {
|
||||||
|
fragments += segment
|
||||||
|
continue
|
||||||
|
} else if fragments != "" {
|
||||||
|
nameParts = append(nameParts, fragments)
|
||||||
|
fragments = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add segment to name.
|
||||||
|
nameParts = append(nameParts, segment)
|
||||||
|
}
|
||||||
|
// Add last fragment.
|
||||||
|
if fragments != "" {
|
||||||
|
nameParts = append(nameParts, fragments)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post-process name parts
|
||||||
|
for i := range nameParts {
|
||||||
|
// Remove any leading delimiters.
|
||||||
|
nameParts[i] = delimiters.ReplaceAllString(nameParts[i], "")
|
||||||
|
|
||||||
|
// Title-case name-only parts.
|
||||||
|
if nameOnly.MatchString(nameParts[i]) {
|
||||||
|
nameParts[i] = strings.Title(nameParts[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(nameParts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanFileDescription(fields []string) string {
|
||||||
|
// If there is a 1 or 2 character delimiter field, only use fields before it.
|
||||||
|
endIndex := len(fields)
|
||||||
|
for i, field := range fields {
|
||||||
|
// Ignore the first field as well as fields with more than two characters.
|
||||||
|
if i >= 1 && len(field) <= 2 && !nameOnly.MatchString(field) {
|
||||||
|
endIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concatenate name
|
||||||
|
binName := strings.Join(fields[:endIndex], " ")
|
||||||
|
|
||||||
|
// If there are multiple sentences, only use the first.
|
||||||
|
if strings.Contains(binName, ". ") {
|
||||||
|
binName = strings.SplitN(binName, ". ", 2)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return binName
|
||||||
|
}
|
15
utils/osdetail/binmeta_default.go
Normal file
15
utils/osdetail/binmeta_default.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
//+build !windows
|
||||||
|
|
||||||
|
package osdetail
|
||||||
|
|
||||||
|
// GetBinaryNameFromSystem queries the operating system for a human readable
|
||||||
|
// name for the given binary path.
|
||||||
|
func GetBinaryNameFromSystem(path string) (string, error) {
|
||||||
|
return "", ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBinaryIconFromSystem queries the operating system for the associated icon
|
||||||
|
// for a given binary path.
|
||||||
|
func GetBinaryIconFromSystem(path string) (string, error) {
|
||||||
|
return "", ErrNotSupported
|
||||||
|
}
|
35
utils/osdetail/binmeta_test.go
Normal file
35
utils/osdetail/binmeta_test.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package osdetail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateBinaryNameFromPath(t *testing.T) {
|
||||||
|
assert.Equal(t, "Nslookup", GenerateBinaryNameFromPath("nslookup.exe"))
|
||||||
|
assert.Equal(t, "System Settings", GenerateBinaryNameFromPath("SystemSettings.exe"))
|
||||||
|
assert.Equal(t, "One Drive Setup", GenerateBinaryNameFromPath("OneDriveSetup.exe"))
|
||||||
|
assert.Equal(t, "Msedge", GenerateBinaryNameFromPath("msedge.exe"))
|
||||||
|
assert.Equal(t, "SIH Client", GenerateBinaryNameFromPath("SIHClient.exe"))
|
||||||
|
assert.Equal(t, "Openvpn Gui", GenerateBinaryNameFromPath("openvpn-gui.exe"))
|
||||||
|
assert.Equal(t, "Portmaster Core v0-1-2", GenerateBinaryNameFromPath("portmaster-core_v0-1-2.exe"))
|
||||||
|
assert.Equal(t, "Win Store App", GenerateBinaryNameFromPath("WinStore.App.exe"))
|
||||||
|
assert.Equal(t, "Test Script", GenerateBinaryNameFromPath(".test-script"))
|
||||||
|
assert.Equal(t, "Browser Broker", GenerateBinaryNameFromPath("browser_broker.exe"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCleanFileDescription(t *testing.T) {
|
||||||
|
assert.Equal(t, "Product Name", cleanFileDescription(strings.Fields("Product Name. Does this and that.")))
|
||||||
|
assert.Equal(t, "Product Name", cleanFileDescription(strings.Fields("Product Name - Does this and that.")))
|
||||||
|
assert.Equal(t, "Product Name", cleanFileDescription(strings.Fields("Product Name / Does this and that.")))
|
||||||
|
assert.Equal(t, "Product Name", cleanFileDescription(strings.Fields("Product Name :: Does this and that.")))
|
||||||
|
assert.Equal(t, "/ Product Name", cleanFileDescription(strings.Fields("/ Product Name")))
|
||||||
|
assert.Equal(t, "Product", cleanFileDescription(strings.Fields("Product / Name")))
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
"Product Name a Does this and that.",
|
||||||
|
cleanFileDescription(strings.Fields("Product Name a Does this and that.")),
|
||||||
|
)
|
||||||
|
}
|
96
utils/osdetail/binmeta_windows.go
Normal file
96
utils/osdetail/binmeta_windows.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package osdetail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const powershellGetFileDescription = `Get-ItemProperty %q | Format-List`
|
||||||
|
|
||||||
|
// GetBinaryNameFromSystem queries the operating system for a human readable
|
||||||
|
// name for the given binary path.
|
||||||
|
func GetBinaryNameFromSystem(path string) (string, error) {
|
||||||
|
// Get FileProperties via Powershell call.
|
||||||
|
output, err := runPowershellCmd(fmt.Sprintf(powershellGetFileDescription, path))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get file properties of %s: %s", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create scanner for the output.
|
||||||
|
scanner := bufio.NewScanner(bytes.NewBufferString(output))
|
||||||
|
scanner.Split(bufio.ScanLines)
|
||||||
|
|
||||||
|
// Search for the FileDescription line.
|
||||||
|
for scanner.Scan() {
|
||||||
|
// Split line up into fields.
|
||||||
|
fields := strings.Fields(scanner.Text())
|
||||||
|
// Discard lines with less than two fields.
|
||||||
|
if len(fields) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Skip all lines that we aren't looking for.
|
||||||
|
if fields[0] != "FileDescription:" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean and return.
|
||||||
|
return cleanFileDescription(fields[1:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a default name as default.
|
||||||
|
return "", ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
const powershellGetIcon = `Add-Type -AssemblyName System.Drawing
|
||||||
|
$Icon = [System.Drawing.Icon]::ExtractAssociatedIcon(%q)
|
||||||
|
$MemoryStream = New-Object System.IO.MemoryStream
|
||||||
|
$Icon.save($MemoryStream)
|
||||||
|
$Bytes = $MemoryStream.ToArray()
|
||||||
|
$MemoryStream.Flush()
|
||||||
|
$MemoryStream.Dispose()
|
||||||
|
[convert]::ToBase64String($Bytes)`
|
||||||
|
|
||||||
|
// TODO: This returns a small and crappy icon.
|
||||||
|
|
||||||
|
// Saving a better icon to file works:
|
||||||
|
/*
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
$ImgList = New-Object System.Windows.Forms.ImageList
|
||||||
|
$ImgList.ImageSize = New-Object System.Drawing.Size(256,256)
|
||||||
|
$ImgList.ColorDepth = 32
|
||||||
|
$Icon = [System.Drawing.Icon]::ExtractAssociatedIcon("C:\Program Files (x86)\Mozilla Firefox\firefox.exe")
|
||||||
|
$ImgList.Images.Add($Icon);
|
||||||
|
$BigIcon = $ImgList.Images.Item(0)
|
||||||
|
$BigIcon.Save("test.png")
|
||||||
|
*/
|
||||||
|
|
||||||
|
// But not saving to a memory stream:
|
||||||
|
/*
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
$ImgList = New-Object System.Windows.Forms.ImageList
|
||||||
|
$ImgList.ImageSize = New-Object System.Drawing.Size(256,256)
|
||||||
|
$ImgList.ColorDepth = 32
|
||||||
|
$Icon = [System.Drawing.Icon]::ExtractAssociatedIcon("C:\Program Files (x86)\Mozilla Firefox\firefox.exe")
|
||||||
|
$ImgList.Images.Add($Icon);
|
||||||
|
$MemoryStream = New-Object System.IO.MemoryStream
|
||||||
|
$BigIcon = $ImgList.Images.Item(0)
|
||||||
|
$BigIcon.Save($MemoryStream)
|
||||||
|
$Bytes = $MemoryStream.ToArray()
|
||||||
|
$MemoryStream.Flush()
|
||||||
|
$MemoryStream.Dispose()
|
||||||
|
[convert]::ToBase64String($Bytes)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// GetBinaryIconFromSystem queries the operating system for the associated icon
|
||||||
|
// for a given binary path and returns it as a data-URL.
|
||||||
|
func GetBinaryIconFromSystem(path string) (string, error) {
|
||||||
|
// Get Associated File Icon via Powershell call.
|
||||||
|
output, err := runPowershellCmd(fmt.Sprintf(powershellGetIcon, path))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get file properties of %s: %s", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "data:image/png;base64," + output, nil
|
||||||
|
}
|
9
utils/osdetail/errors.go
Normal file
9
utils/osdetail/errors.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package osdetail
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotSupported = errors.New("not supported")
|
||||||
|
ErrNotFound = errors.New("not found")
|
||||||
|
ErrEmptyOutput = errors.New("command succeeded with empty output")
|
||||||
|
)
|
47
utils/osdetail/powershell_windows.go
Normal file
47
utils/osdetail/powershell_windows.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
package osdetail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runPowershellCmd(script string) (output string, err error) {
|
||||||
|
// Create command to execute.
|
||||||
|
cmd := exec.Command(
|
||||||
|
"powershell.exe",
|
||||||
|
"-NoProfile",
|
||||||
|
"-NonInteractive",
|
||||||
|
script,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create and assign output buffers.
|
||||||
|
var stdoutBuf bytes.Buffer
|
||||||
|
var stderrBuf bytes.Buffer
|
||||||
|
cmd.Stdout = &stdoutBuf
|
||||||
|
cmd.Stderr = &stderrBuf
|
||||||
|
|
||||||
|
// Run command and collect output.
|
||||||
|
err = cmd.Run()
|
||||||
|
stdout, stderr := stdoutBuf.String(), stderrBuf.String()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// Powershell might not return an error, but just write to stdout instead.
|
||||||
|
if stderr != "" {
|
||||||
|
return "", errors.New(strings.SplitN(stderr, "\n", 2)[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugging output:
|
||||||
|
// fmt.Printf("powershell stdout: %s\n", stdout)
|
||||||
|
// fmt.Printf("powershell stderr: %s\n", stderr)
|
||||||
|
|
||||||
|
// Finalize stdout.
|
||||||
|
cleanedOutput := strings.TrimSpace(stdout)
|
||||||
|
if cleanedOutput == "" {
|
||||||
|
return "", ErrEmptyOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanedOutput, nil
|
||||||
|
}
|
|
@ -7,9 +7,42 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
fmt.Println("Binary Names:")
|
||||||
|
printBinaryName("openvpn-gui.exe", `C:\Program Files\OpenVPN\bin\openvpn-gui.exe`)
|
||||||
|
printBinaryName("firefox.exe", `C:\Program Files (x86)\Mozilla Firefox\firefox.exe`)
|
||||||
|
printBinaryName("powershell.exe", `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`)
|
||||||
|
printBinaryName("explorer.exe", `C:\Windows\explorer.exe`)
|
||||||
|
printBinaryName("svchost.exe", `C:\Windows\System32\svchost.exe`)
|
||||||
|
|
||||||
|
fmt.Println("\n\nBinary Icons:")
|
||||||
|
printBinaryIcon("openvpn-gui.exe", `C:\Program Files\OpenVPN\bin\openvpn-gui.exe`)
|
||||||
|
printBinaryIcon("firefox.exe", `C:\Program Files (x86)\Mozilla Firefox\firefox.exe`)
|
||||||
|
printBinaryIcon("powershell.exe", `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`)
|
||||||
|
printBinaryIcon("explorer.exe", `C:\Windows\explorer.exe`)
|
||||||
|
printBinaryIcon("svchost.exe", `C:\Windows\System32\svchost.exe`)
|
||||||
|
|
||||||
|
fmt.Println("\n\nSvcHost Service Names:")
|
||||||
names, err := osdetail.GetAllServiceNames()
|
names, err := osdetail.GetAllServiceNames()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%+v\n", names)
|
fmt.Printf("%+v\n", names)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printBinaryName(name, path string) {
|
||||||
|
binName, err := osdetail.GetBinaryName(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("%s: ERROR: %s\n", name, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s: %s\n", name, binName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printBinaryIcon(name, path string) {
|
||||||
|
binIcon, err := osdetail.GetBinaryIcon(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("%s: ERROR: %s\n", name, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s: %s\n", name, binIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue