- Added SRI hash injection during frontend build to improve security. - Updated ESLint configuration to include 'navigator' as a global variable. - Introduced a new `settingsStore` to manage user preferences for asset verification. - Enhanced `SoftwareCard` and `VerificationModal` components to display contributor information and security checks. - Updated `verificationStore` to handle expanded toast notifications for detailed verification steps. - Implemented a new `CodeBlock` component for displaying code snippets with syntax highlighting. - Improved API documentation and added new endpoints for fetching software and asset details.
415 lines
9.5 KiB
Go
415 lines
9.5 KiB
Go
package gitea
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"software-station/internal/models"
|
|
)
|
|
|
|
func CheckSourceSecurity(serverURL, token, owner, repo string) *models.SourceSecurity {
|
|
u, err := url.Parse(serverURL)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
domain := u.Hostname()
|
|
|
|
security := &models.SourceSecurity{
|
|
Domain: domain,
|
|
}
|
|
|
|
// TLS Check
|
|
conf := &tls.Config{
|
|
InsecureSkipVerify: false,
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
// Use port 443 for HTTPS domains
|
|
port := "443"
|
|
if u.Port() != "" {
|
|
port = u.Port()
|
|
}
|
|
|
|
dialer := &net.Dialer{Timeout: 5 * time.Second}
|
|
conn, err := tls.DialWithDialer(dialer, "tcp", net.JoinHostPort(domain, port), conf)
|
|
if err == nil {
|
|
security.TLSValid = true
|
|
_ = conn.Close()
|
|
}
|
|
|
|
return security
|
|
}
|
|
|
|
func FetchUserGPGKeys(server, token, username string) ([]string, error) {
|
|
url := fmt.Sprintf("%s/api/v1/users/%s/gpg_keys", server, username)
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if token != "" {
|
|
req.Header.Set("Authorization", "token "+token)
|
|
}
|
|
|
|
client := &http.Client{Timeout: DefaultTimeout}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("gitea gpg api returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var giteaKeys []struct {
|
|
PublicKey string `json:"public_key"`
|
|
}
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&giteaKeys); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
keys := make([]string, len(giteaKeys))
|
|
for i, k := range giteaKeys {
|
|
keys[i] = k.PublicKey
|
|
}
|
|
|
|
return keys, nil
|
|
}
|
|
|
|
func FetchContributors(server, token, owner, repo string) ([]models.Contributor, error) {
|
|
url := fmt.Sprintf("%s%s/%s/%s%s", server, RepoAPIPath, owner, repo, ContributorsSuffix)
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if token != "" {
|
|
req.Header.Set("Authorization", "token "+token)
|
|
}
|
|
|
|
client := &http.Client{Timeout: DefaultTimeout}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("gitea api returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var giteaContributors []struct {
|
|
Username string `json:"username"`
|
|
AvatarURL string `json:"avatar_url"`
|
|
}
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&giteaContributors); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
contributors := make([]models.Contributor, len(giteaContributors))
|
|
for i, c := range giteaContributors {
|
|
keys, _ := FetchUserGPGKeys(server, token, c.Username)
|
|
contributors[i] = models.Contributor{
|
|
Username: c.Username,
|
|
AvatarURL: c.AvatarURL,
|
|
GPGKeys: keys,
|
|
}
|
|
}
|
|
|
|
return contributors, nil
|
|
}
|
|
|
|
func DetectOS(filename string) string {
|
|
lower := strings.ToLower(filename)
|
|
|
|
osMap := []struct {
|
|
patterns []string
|
|
suffixes []string
|
|
os string
|
|
}{
|
|
{
|
|
patterns: []string{"windows"},
|
|
suffixes: []string{".exe", ".msi"},
|
|
os: models.OSWindows,
|
|
},
|
|
{
|
|
patterns: []string{"linux"},
|
|
suffixes: []string{".deb", ".rpm", ".appimage", ".flatpak"},
|
|
os: models.OSLinux,
|
|
},
|
|
{
|
|
patterns: []string{"mac", "darwin"},
|
|
suffixes: []string{".dmg", ".pkg"},
|
|
os: models.OSMacOS,
|
|
},
|
|
{
|
|
patterns: []string{"freebsd"},
|
|
os: models.OSFreeBSD,
|
|
},
|
|
{
|
|
patterns: []string{"openbsd"},
|
|
os: models.OSOpenBSD,
|
|
},
|
|
{
|
|
patterns: []string{"android"},
|
|
suffixes: []string{".apk"},
|
|
os: models.OSAndroid,
|
|
},
|
|
{
|
|
patterns: []string{"arm", "aarch64"},
|
|
os: models.OSARM,
|
|
},
|
|
}
|
|
|
|
for _, entry := range osMap {
|
|
for _, p := range entry.patterns {
|
|
if strings.Contains(lower, p) {
|
|
return entry.os
|
|
}
|
|
}
|
|
for _, s := range entry.suffixes {
|
|
if strings.HasSuffix(lower, s) {
|
|
return entry.os
|
|
}
|
|
}
|
|
}
|
|
|
|
return models.OSUnknown
|
|
}
|
|
|
|
func FetchRepoInfo(server, token, owner, repo string) (string, []string, string, bool, string, error) {
|
|
url := fmt.Sprintf("%s%s/%s/%s", server, RepoAPIPath, owner, repo)
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return "", nil, "", false, "", err
|
|
}
|
|
|
|
if token != "" {
|
|
req.Header.Set("Authorization", "token "+token)
|
|
}
|
|
|
|
client := &http.Client{Timeout: DefaultTimeout}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", nil, "", false, "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", nil, "", false, "", fmt.Errorf("gitea api returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var info struct {
|
|
Description string `json:"description"`
|
|
Topics []string `json:"topics"`
|
|
DefaultBranch string `json:"default_branch"`
|
|
Licenses []string `json:"licenses"`
|
|
Private bool `json:"private"`
|
|
AvatarURL string `json:"avatar_url"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
|
return "", nil, "", false, "", err
|
|
}
|
|
|
|
license := ""
|
|
if len(info.Licenses) > 0 {
|
|
license = info.Licenses[0]
|
|
}
|
|
if license == "" {
|
|
// Try to detect license from file if API returns nothing
|
|
license = detectLicenseFromFile(server, token, owner, repo, info.DefaultBranch)
|
|
}
|
|
|
|
return info.Description, info.Topics, license, info.Private, info.AvatarURL, nil
|
|
}
|
|
|
|
func detectLicenseFromFile(server, token, owner, repo, defaultBranch string) string {
|
|
branches := []string{"main", "master"}
|
|
if defaultBranch != "" {
|
|
branches = append([]string{defaultBranch}, "main", "master")
|
|
}
|
|
seen := make(map[string]bool)
|
|
var finalBranches []string
|
|
for _, b := range branches {
|
|
if !seen[b] {
|
|
seen[b] = true
|
|
finalBranches = append(finalBranches, b)
|
|
}
|
|
}
|
|
|
|
for _, branch := range finalBranches {
|
|
url := fmt.Sprintf("%s/%s/%s/raw/branch/%s/LICENSE", server, owner, repo, branch)
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if token != "" {
|
|
req.Header.Set("Authorization", "token "+token)
|
|
}
|
|
|
|
client := &http.Client{Timeout: DefaultTimeout}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
scanner := bufio.NewScanner(resp.Body)
|
|
for i := 0; i < 5 && scanner.Scan(); i++ {
|
|
line := strings.ToUpper(scanner.Text())
|
|
if strings.Contains(line, "MIT LICENSE") {
|
|
return "MIT"
|
|
}
|
|
if strings.Contains(line, "GNU GENERAL PUBLIC LICENSE") || strings.Contains(line, "GPL") {
|
|
return "GPL"
|
|
}
|
|
if strings.Contains(line, "APACHE LICENSE") {
|
|
return "Apache-2.0"
|
|
}
|
|
if strings.Contains(line, "BSD") {
|
|
return "BSD"
|
|
}
|
|
}
|
|
return "LICENSE"
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func IsSBOM(filename string) bool {
|
|
lower := strings.ToLower(filename)
|
|
return strings.Contains(lower, "sbom") ||
|
|
strings.Contains(lower, "cyclonedx") ||
|
|
strings.Contains(lower, "spdx") ||
|
|
strings.HasSuffix(lower, ".cdx.json") ||
|
|
strings.HasSuffix(lower, ".spdx.json")
|
|
}
|
|
|
|
func FetchReleases(server, token, owner, repo string) ([]models.Release, error) {
|
|
url := fmt.Sprintf("%s%s/%s/%s%s", server, RepoAPIPath, owner, repo, ReleasesSuffix)
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if token != "" {
|
|
req.Header.Set("Authorization", "token "+token)
|
|
}
|
|
|
|
client := &http.Client{Timeout: DefaultTimeout}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("gitea api returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var giteaReleases []struct {
|
|
TagName string `json:"tag_name"`
|
|
Body string `json:"body"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Assets []struct {
|
|
Name string `json:"name"`
|
|
Size int64 `json:"size"`
|
|
URL string `json:"browser_download_url"`
|
|
} `json:"assets"`
|
|
}
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&giteaReleases); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
contributors, _ := FetchContributors(server, token, owner, repo)
|
|
security := CheckSourceSecurity(server, token, owner, repo)
|
|
|
|
var releases []models.Release
|
|
for _, gr := range giteaReleases {
|
|
var assets []models.Asset
|
|
var checksumsURL string
|
|
|
|
for _, ga := range gr.Assets {
|
|
if ga.Name == "SHA256SUMS" {
|
|
checksumsURL = ga.URL
|
|
continue
|
|
}
|
|
assets = append(assets, models.Asset{
|
|
Name: ga.Name,
|
|
Size: ga.Size,
|
|
URL: ga.URL,
|
|
OS: DetectOS(ga.Name),
|
|
IsSBOM: IsSBOM(ga.Name),
|
|
})
|
|
}
|
|
|
|
if checksumsURL != "" {
|
|
checksums, err := fetchAndParseChecksums(checksumsURL, token)
|
|
if err == nil {
|
|
for i := range assets {
|
|
if sha, ok := checksums[assets[i].Name]; ok {
|
|
assets[i].SHA256 = sha
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
releases = append(releases, models.Release{
|
|
TagName: gr.TagName,
|
|
Body: gr.Body,
|
|
CreatedAt: gr.CreatedAt,
|
|
Assets: assets,
|
|
Contributors: contributors,
|
|
Security: security,
|
|
})
|
|
}
|
|
|
|
return releases, nil
|
|
}
|
|
|
|
func fetchAndParseChecksums(url, token string) (map[string]string, error) {
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if token != "" {
|
|
req.Header.Set("Authorization", "token "+token)
|
|
}
|
|
|
|
client := &http.Client{Timeout: DefaultTimeout}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("failed to fetch checksums: %d", resp.StatusCode)
|
|
}
|
|
|
|
checksums := make(map[string]string)
|
|
scanner := bufio.NewScanner(resp.Body)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
parts := strings.Fields(line)
|
|
if len(parts) >= 2 {
|
|
checksums[parts[1]] = parts[0]
|
|
}
|
|
}
|
|
return checksums, nil
|
|
}
|