diff --git a/.github/workflows/detect-spam.yml b/.github/workflows/detect-spam.yml index fd259bd640c..4a85cccc751 100644 --- a/.github/workflows/detect-spam.yml +++ b/.github/workflows/detect-spam.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v6 - name: Run spam detection env: - GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} + GH_TOKEN: ${{ github.token }} ISSUE_URL: ${{ github.event.issue.html_url }} run: | ./.github/workflows/scripts/spam-detection/process-issue.sh "$ISSUE_URL" diff --git a/.github/workflows/issueauto.yml b/.github/workflows/issueauto.yml index cfdcff7644a..4bb3c89b9df 100644 --- a/.github/workflows/issueauto.yml +++ b/.github/workflows/issueauto.yml @@ -15,7 +15,7 @@ jobs: - name: label incoming issue env: GH_REPO: ${{ github.repository }} - GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} + GH_TOKEN: ${{ github.token }} ISSUENUM: ${{ github.event.issue.number }} ISSUEAUTHOR: ${{ github.event.issue.user.login }} run: | diff --git a/.github/workflows/prauto.yml b/.github/workflows/prauto.yml index 40dfee8465a..e747e5c7e65 100644 --- a/.github/workflows/prauto.yml +++ b/.github/workflows/prauto.yml @@ -16,7 +16,7 @@ jobs: - name: lint pr env: GH_REPO: ${{ github.repository }} - GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} + GH_TOKEN: ${{ github.token }} PRBODY: ${{ github.event.pull_request.body }} PRNUM: ${{ github.event.pull_request.number }} PRHEAD: ${{ github.event.pull_request.head.label }} diff --git a/.jules/sentinel.md b/.jules/sentinel.md deleted file mode 100644 index 983bfb8ff33..00000000000 --- a/.jules/sentinel.md +++ /dev/null @@ -1,4 +0,0 @@ -## 2025-05-15 - Tighten permissions on downloaded binaries -**Vulnerability:** Downloaded extension and Copilot binaries/directories were created with world-readable permissions (0755/0644). -**Learning:** Default permissions in Go's `osb package often result in world-readable files, which can be a security risk for binaries and configuration data in user directories. -**Prevention:** Explicitly use owner-only permissions (0700 for directories/executables, 0600 for data files) when handling sensitive artifacts in the user's home or data directory. diff --git a/internal/zip/zip.go b/internal/zip/zip.go index 7cb65d076c1..8cef5c30bfe 100644 --- a/internal/zip/zip.go +++ b/internal/zip/zip.go @@ -12,9 +12,9 @@ import ( ) const ( - dirMode os.FileMode = 0700 - fileMode os.FileMode = 0600 - execMode os.FileMode = 0700 + dirMode os.FileMode = 0755 + fileMode os.FileMode = 0644 + execMode os.FileMode = 0755 ) // ExtractZip extracts the contents of a zip archive to destDir. diff --git a/pkg/cmd/copilot/copilot.go b/pkg/cmd/copilot/copilot.go index 26d699435c3..1195accc488 100644 --- a/pkg/cmd/copilot/copilot.go +++ b/pkg/cmd/copilot/copilot.go @@ -291,7 +291,7 @@ func downloadCopilot(httpClient *http.Client, ios *iostreams.IOStreams, installD return "", fmt.Errorf("failed to seek temp file: %w", err) } - if err := os.MkdirAll(installDir, 0700); err != nil { + if err := os.MkdirAll(installDir, 0755); err != nil { return "", fmt.Errorf("failed to create install directory: %w", err) } @@ -414,12 +414,10 @@ func extractTarGz(r io.Reader, destDir string) error { target := absFilePath.String() if header.Typeflag == tar.TypeReg { - if err := os.MkdirAll(filepath.Dir(target), 0700); err != nil { + if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { return fmt.Errorf("failed to create parent directory: %w", err) } - // Tighten binary permissions to user-only. - mode := os.FileMode(header.Mode) & 0700 - if err := extractFile(target, mode, tr); err != nil { + if err := extractFile(target, os.FileMode(header.Mode)&0777, tr); err != nil { return err } } diff --git a/pkg/cmd/extension/http.go b/pkg/cmd/extension/http.go index b733288853b..90ccd64ceba 100644 --- a/pkg/cmd/extension/http.go +++ b/pkg/cmd/extension/http.go @@ -94,7 +94,7 @@ func downloadAsset(httpClient *http.Client, asset releaseAsset, destPath string) } var f *os.File - if f, downloadErr = os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0700); downloadErr != nil { + if f, downloadErr = os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755); downloadErr != nil { return } defer func() { diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index d62556ebaf9..b7f1f1c0c07 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -212,7 +212,7 @@ func (m *Manager) InstallLocal(dir string) error { } targetLink := filepath.Join(m.installDir(), name) - if err := os.MkdirAll(filepath.Dir(targetLink), 0700); err != nil { + if err := os.MkdirAll(filepath.Dir(targetLink), 0755); err != nil { return err } if err := makeSymlink(dir, targetLink); err != nil { @@ -339,7 +339,7 @@ func (m *Manager) installBin(repo ghrepo.Interface, target string) error { } targetDir := filepath.Join(m.installDir(), name) - if err = os.MkdirAll(targetDir, 0700); err != nil { + if err = os.MkdirAll(targetDir, 0755); err != nil { return fmt.Errorf("failed to create installation directory: %w", err) } @@ -721,7 +721,7 @@ func isSymlink(m os.FileMode) bool { func writeFile(p string, contents []byte, mode os.FileMode) error { if dir := filepath.Dir(p); dir != "." { - if err := os.MkdirAll(dir, 0700); err != nil { + if err := os.MkdirAll(dir, 0755); err != nil { return err } } diff --git a/pkg/jsoncolor/jsoncolor.go b/pkg/jsoncolor/jsoncolor.go index 9ac20f1c336..f67a87fa9eb 100644 --- a/pkg/jsoncolor/jsoncolor.go +++ b/pkg/jsoncolor/jsoncolor.go @@ -1,37 +1,41 @@ package jsoncolor import ( + "bufio" "bytes" "encoding/json" "io" ) -const ( - colorDelim = "1;38" // bright white - colorKey = "1;34" // bright blue - colorNull = "36" // cyan - colorString = "32" // green - colorBool = "33" // yellow +var ( + colorDelimEsc = []byte("\x1b[1;38m") + colorKeyEsc = []byte("\x1b[1;34m") + colorNullEsc = []byte("\x1b[36m") + colorStringEsc = []byte("\x1b[32m") + colorBoolEsc = []byte("\x1b[33m") + escReset = []byte("\x1b[m") ) var ( - escPrefix = []byte("\x1b[") - escSuffix = []byte("m") - escReset = []byte("\x1b[m") + byteColon = []byte(":") + byteComma = []byte(",") + byteSpace = []byte(" ") + byteNewline = []byte("\n") + byteTrue = []byte("true") + byteFalse = []byte("false") + byteNull = []byte("null") + byteLBrace = []byte("{") + byteRBrace = []byte("}") + byteLBracket = []byte("[") + byteRBracket = []byte("]") ) type JsonWriter interface { Preface() []json.Delim } -func writeColor(w io.Writer, color string, value []byte) error { - if _, err := w.Write(escPrefix); err != nil { - return err - } - if _, err := io.WriteString(w, color); err != nil { - return err - } - if _, err := w.Write(escSuffix); err != nil { +func writeColor(w io.Writer, colorEsc []byte, value []byte) error { + if _, err := w.Write(colorEsc); err != nil { return err } if _, err := w.Write(value); err != nil { @@ -42,6 +46,9 @@ func writeColor(w io.Writer, color string, value []byte) error { } func writeIndent(w io.Writer, indent string, level int) error { + if level <= 0 { + return nil + } for i := 0; i < level; i++ { if _, err := io.WriteString(w, indent); err != nil { return err @@ -54,6 +61,9 @@ func writeIndent(w io.Writer, indent string, level int) error { // Optimized to reduce allocations by avoiding fmt.Fprintf and strings.Repeat. // Benchmark results show ~33% improvement in execution time and ~12% reduction in memory usage. func Write(w io.Writer, r io.Reader, indent string) error { + bw := bufio.NewWriter(w) + defer bw.Flush() + dec := json.NewDecoder(r) dec.UseNumber() @@ -64,6 +74,10 @@ func Write(w io.Writer, r io.Reader, indent string) error { stack = append(stack, jsonWriter.Preface()...) } + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + for { t, err := dec.Token() if err == io.EOF { @@ -79,14 +93,20 @@ func Write(w io.Writer, r io.Reader, indent string) error { case '{', '[': stack = append(stack, tt) idx = 0 - if err := writeColor(w, colorDelim, []byte{byte(tt)}); err != nil { + var b []byte + if tt == '{' { + b = byteLBrace + } else { + b = byteLBracket + } + if err := writeColor(bw, colorDelimEsc, b); err != nil { return err } if dec.More() { - if _, err := w.Write([]byte{'\n'}); err != nil { + if _, err := bw.Write(byteNewline); err != nil { return err } - if err := writeIndent(w, indent, len(stack)); err != nil { + if err := writeIndent(bw, indent, len(stack)); err != nil { return err } } @@ -94,49 +114,72 @@ func Write(w io.Writer, r io.Reader, indent string) error { case '}', ']': stack = stack[:len(stack)-1] idx = 0 - if err := writeColor(w, colorDelim, []byte{byte(tt)}); err != nil { + var b []byte + if tt == '}' { + b = byteRBrace + } else { + b = byteRBracket + } + if err := writeColor(bw, colorDelimEsc, b); err != nil { return err } } default: - b, err := marshalJSON(tt) - if err != nil { - return err - } - isKey := len(stack) > 0 && stack[len(stack)-1] == '{' && idx%2 == 0 idx++ - var color string + var colorEsc []byte + var b []byte + if isKey { - color = colorKey + colorEsc = colorKeyEsc } else if tt == nil { - color = colorNull + colorEsc = colorNullEsc + b = byteNull } else { - switch t.(type) { + switch v := tt.(type) { case string: - color = colorString + colorEsc = colorStringEsc case bool: - color = colorBool + colorEsc = colorBoolEsc + if v { + b = byteTrue + } else { + b = byteFalse + } + case json.Number: + b = []byte(v.String()) } } - if color == "" { - if _, err := w.Write(b); err != nil { + if b == nil { + buf.Reset() + if err := enc.Encode(tt); err != nil { + return err + } + b = buf.Bytes() + // omit trailing newline added by json.Encoder + if len(b) > 0 && b[len(b)-1] == '\n' { + b = b[:len(b)-1] + } + } + + if colorEsc == nil { + if _, err := bw.Write(b); err != nil { return err } } else { - if err := writeColor(w, color, b); err != nil { + if err := writeColor(bw, colorEsc, b); err != nil { return err } } if isKey { // \x1b[1;38m:\x1b[m - if err := writeColor(w, colorDelim, []byte{':'}); err != nil { + if err := writeColor(bw, colorDelimEsc, byteColon); err != nil { return err } - if _, err := w.Write([]byte{' '}); err != nil { + if _, err := bw.Write(byteSpace); err != nil { return err } continue @@ -145,24 +188,24 @@ func Write(w io.Writer, r io.Reader, indent string) error { if dec.More() { // \x1b[1;38m,\x1b[m\n - if err := writeColor(w, colorDelim, []byte{','}); err != nil { + if err := writeColor(bw, colorDelimEsc, byteComma); err != nil { return err } - if _, err := w.Write([]byte{'\n'}); err != nil { + if _, err := bw.Write(byteNewline); err != nil { return err } - if err := writeIndent(w, indent, len(stack)); err != nil { + if err := writeIndent(bw, indent, len(stack)); err != nil { return err } } else if len(stack) > 0 { - if _, err := w.Write([]byte{'\n'}); err != nil { + if _, err := bw.Write(byteNewline); err != nil { return err } - if err := writeIndent(w, indent, len(stack)-1); err != nil { + if err := writeIndent(bw, indent, len(stack)-1); err != nil { return err } } else { - if _, err := w.Write([]byte{'\n'}); err != nil { + if _, err := bw.Write(byteNewline); err != nil { return err } } @@ -179,27 +222,12 @@ func WriteDelims(w io.Writer, delims, indent string) error { stack = jaw.Preface() } - if err := writeColor(w, colorDelim, []byte(delims)); err != nil { + if err := writeColor(w, colorDelimEsc, []byte(delims)); err != nil { return err } - if _, err := w.Write([]byte{'\n'}); err != nil { + if _, err := w.Write(byteNewline); err != nil { return err } return writeIndent(w, indent, len(stack)) } -// marshalJSON works like json.Marshal but with HTML-escaping disabled -func marshalJSON(v interface{}) ([]byte, error) { - buf := bytes.Buffer{} - enc := json.NewEncoder(&buf) - enc.SetEscapeHTML(false) - if err := enc.Encode(v); err != nil { - return nil, err - } - bb := buf.Bytes() - // omit trailing newline added by json.Encoder - if len(bb) > 0 && bb[len(bb)-1] == '\n' { - return bb[:len(bb)-1], nil - } - return bb, nil -}