package diagnostics import ( "encoding/json" "fmt" "log/slog" "os" "strings" "time" "github.com/github/codeql-go/extractor/util" ) type sourceStruct struct { Id string `json:"id"` Name string `json:"name"` ExtractorName string `json:"extractorName"` } type diagnosticSeverity string const ( severityError diagnosticSeverity = "error" severityWarning diagnosticSeverity = "warning" severityNote diagnosticSeverity = "note" ) type visibilityStruct struct { StatusPage bool `json:"statusPage"` // True if the message should be displayed on the status page (defaults to false) CliSummaryTable bool `json:"cliSummaryTable"` // True if the message should be counted in the diagnostics summary table printed by `codeql database analyze` (defaults to false) Telemetry bool `json:"telemetry"` // True if the message should be sent to telemetry (defaults to false) } var fullVisibility *visibilityStruct = &visibilityStruct{true, true, true} var telemetryOnly *visibilityStruct = &visibilityStruct{false, false, true} type locationStruct struct { File string `json:"file,omitempty"` StartLine int `json:"startLine,omitempty"` StartColumn int `json:"startColumn,omitempty"` EndLine int `json:"endLine,omitempty"` EndColumn int `json:"endColumn,omitempty"` } var noLocation *locationStruct = nil type diagnostic struct { Timestamp string `json:"timestamp"` Source sourceStruct `json:"source"` MarkdownMessage string `json:"markdownMessage"` Severity string `json:"severity"` Visibility *visibilityStruct `json:"visibility,omitempty"` // Use a pointer so that it is omitted if nil Location *locationStruct `json:"location,omitempty"` // Use a pointer so that it is omitted if nil } var diagnosticsEmitted, diagnosticsLimit uint = 0, 100 var noDiagnosticDirPrinted bool = false type DiagnosticsWriter interface { WriteDiagnostic(d diagnostic) } type FileDiagnosticsWriter struct { diagnosticDir string } func (writer *FileDiagnosticsWriter) WriteDiagnostic(d diagnostic) { if writer == nil { return } content, err := json.Marshal(d) if err != nil { slog.Error("Failed to encode diagnostic as JSON", slog.Any("err", err)) return } targetFile, err := os.CreateTemp(writer.diagnosticDir, "go-extractor.*.json") if err != nil { slog.Error("Failed to create diagnostic file", slog.Any("err", err)) return } defer func() { if err := targetFile.Close(); err != nil { slog.Error("Failed to close diagnostic file", slog.Any("err", err)) } }() _, err = targetFile.Write(content) if err != nil { slog.Error("Failed to write to diagnostic file", slog.Any("err", err)) } } var DefaultWriter *FileDiagnosticsWriter = nil func NewFileDiagnosticsWriter() *FileDiagnosticsWriter { diagnosticDir := os.Getenv("CODEQL_EXTRACTOR_GO_DIAGNOSTIC_DIR") if diagnosticDir == "" { if !noDiagnosticDirPrinted { slog.Warn("No diagnostic directory set, so not emitting diagnostics") noDiagnosticDirPrinted = true } return nil } return &FileDiagnosticsWriter{diagnosticDir} } func init() { DefaultWriter = NewFileDiagnosticsWriter() } // Emits a diagnostic using the specified `DiagnosticsWriter`. func emitDiagnosticTo(writer DiagnosticsWriter, sourceid, sourcename, markdownMessage string, severity diagnosticSeverity, visibility *visibilityStruct, location *locationStruct) { if diagnosticsEmitted < diagnosticsLimit { diagnosticsEmitted += 1 timestamp := time.Now().UTC().Format("2006-01-02T15:04:05.000") + "Z" var d diagnostic if diagnosticsEmitted < diagnosticsLimit { d = diagnostic{ timestamp, sourceStruct{sourceid, sourcename, "go"}, markdownMessage, string(severity), visibility, location, } } else { d = diagnostic{ timestamp, sourceStruct{"go/autobuilder/diagnostic-limit-reached", "Diagnostics limit exceeded", "go"}, fmt.Sprintf("CodeQL has produced more than the maximum number of diagnostics. Only the first %d have been reported.", diagnosticsLimit), string(severityWarning), fullVisibility, noLocation, } } writer.WriteDiagnostic(d) } } // Emits a diagnostic using the default `DiagnosticsWriter`. func emitDiagnostic(sourceid, sourcename, markdownMessage string, severity diagnosticSeverity, visibility *visibilityStruct, location *locationStruct) { emitDiagnosticTo(DefaultWriter, sourceid, sourcename, markdownMessage, severity, visibility, location) } func EmitPackageDifferentOSArchitecture(pkgPath string) { emitDiagnostic( "go/autobuilder/package-different-os-architecture", "An imported package is intended for a different OS or architecture", "`"+pkgPath+"` could not be imported. Make sure the `GOOS` and `GOARCH` [environment variables are correctly set](https://docs.github.com/en/actions/learn-github-actions/variables#defining-environment-variables-for-a-single-workflow). Alternatively, [change your OS and architecture](https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#using-a-github-hosted-runner).", severityWarning, fullVisibility, noLocation, ) } func plural(n int, singular, plural string) string { if n == 1 { return singular } else { return plural } } const maxNumPkgPaths = 5 func EmitCannotFindPackages(writer DiagnosticsWriter, pkgPaths []string) { numPkgPaths := len(pkgPaths) numPrinted := numPkgPaths truncated := false if numPrinted > maxNumPkgPaths { numPrinted = maxNumPkgPaths truncated = true } secondLine := "`" + strings.Join(pkgPaths[0:numPrinted], "`, `") + "`" if truncated { secondLine += fmt.Sprintf(" and %d more", numPkgPaths-maxNumPkgPaths) } message := fmt.Sprintf( "%d package%s could not be found:\n\n%s.\n\n"+ "CodeQL is able to analyze your code without those packages, but definitions from them may not be recognized and "+ "source files that use them may only be partially analyzed.\n\n"+ "To ensure that you have comprehensive alert coverage, check that the paths are correct and make sure any private packages can be accessed by CodeQL. ", numPkgPaths, plural(len(pkgPaths), "", "s"), secondLine, ) // Depending on the environment we are running in, provide a different message for how to configure access to private registries. if util.IsDynamicActionsWorkflow() { // For GitHub-managed (dynamic) workflows, we offer built-in support for private registries that customers can set up. message = message + "Organizations [can grant access to private registries for GitHub security products](https://docs.github.com/en/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries). " } else { if util.IsActionsWorkflow() { // For custom workflows, users can add a workflow step to set up credentials or environment variables. message = message + "To set up access to a private registry, add a step to your workflow which sets up the necessary credentials and environment variables. " } else { // Otherwise, we are running locally or in some other CI system. message = message + "To set up access to private registries, ensure that the necessary credentials and environment variables are set up for `go` to use. " } // This should be less likely since we improved Go project discovery. We only include it in the message if we are not running in a // GitHub-managed workflow, since users would not be able to act on this there. message = message + "If any of the packages are already present in the repository, but were not found, then you may need a [custom build command](https://docs.github.com/en/code-security/how-tos/scan-code-for-vulnerabilities/manage-your-configuration/codeql-code-scanning-for-compiled-languages)." } emitDiagnosticTo( writer, "go/autobuilder/package-not-found", "Some packages could not be found", message, severityWarning, fullVisibility, noLocation, ) } func EmitNewerGoVersionNeeded(installedVersion string, requiredVersion string) { emitDiagnostic( "go/autobuilder/newer-go-version-needed", "Newer Go version needed", "Version `"+installedVersion+"` of Go is installed, but this is lower than `"+requiredVersion+"` required by your project's `go.mod`. [Install a newer version of Go before analyzing your project](https://github.com/actions/setup-go#basic).", severityError, fullVisibility, noLocation, ) } func EmitGoFilesFoundButNotProcessed() { emitDiagnostic( "go/autobuilder/go-files-found-but-not-processed", "Go files were found but not processed", "[Specify a custom build command](https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-the-codeql-workflow-for-compiled-languages) that includes one or more `go build` commands to build the `.go` files to be analyzed.", severityError, fullVisibility, noLocation, ) } func EmitRelativeImportPaths() { emitDiagnostic( "go/autobuilder/relative-import-paths", "Some imports use unsupported relative package paths", "You should replace relative package paths (that contain `.` or `..`) with absolute paths. Alternatively you can [use a Go module](https://go.dev/blog/using-go-modules).", severityError, fullVisibility, noLocation, ) } // The following diagnostics are telemetry-only. func EmitBazelBuildFilesFound(bazelPaths []string) { emitDiagnostic( "go/autobuilder/bazel-build-file-found", "Bazel BUILD files were found", fmt.Sprintf( "%d bazel BUILD %s found:\n\n`%s`", len(bazelPaths), plural(len(bazelPaths), "file was", "files were"), strings.Join(bazelPaths, "`, `")), severityNote, telemetryOnly, noLocation, ) } func EmitGopkgTomlFound() { emitDiagnostic( "go/autobuilder/gopkg-toml-found", "A dep `Gopkg.toml` file was found", "A dep `Gopkg.toml` file was found", severityNote, telemetryOnly, noLocation, ) } func EmitGlideYamlFound() { emitDiagnostic( "go/autobuilder/glide-yaml-found", "A Glide `glide.yaml` file was found", "A Glide `glide.yaml` file was found", severityNote, telemetryOnly, noLocation, ) } func EmitGoWorkFound(goWorkPaths []string) { emitDiagnostic( "go/autobuilder/go-work-found", "`go.work` file found", fmt.Sprintf( "%d `go.work` %s found:\n\n`%s`", len(goWorkPaths), plural(len(goWorkPaths), "file was", "files were"), strings.Join(goWorkPaths, "`, `")), severityNote, telemetryOnly, noLocation, ) } func EmitGoFilesOutsideGoModules(goModPaths []string) { emitDiagnostic( "go/autobuilder/go-files-outside-go-modules", "Go files were found outside Go modules", "Go files were found outside of the Go modules corresponding to these `go.mod` files.\n\n`"+strings.Join(goModPaths, "`, `")+"`", severityNote, telemetryOnly, noLocation, ) } func EmitMultipleGoModFoundNested(goModPaths []string) { emitDiagnostic( "go/autobuilder/multiple-go-mod-found-nested", "Multiple `go.mod` files were found, all nested under one root `go.mod` file", fmt.Sprintf( "%d `go.mod` files were found:\n\n`%s`", len(goModPaths), strings.Join(goModPaths, "`, `")), severityNote, telemetryOnly, noLocation, ) } func EmitMultipleGoModFoundNotNested(goModPaths []string) { emitDiagnostic( "go/autobuilder/multiple-go-mod-found-not-nested", "Multiple `go.mod` files found, not all nested under one root `go.mod` file", fmt.Sprintf( "%d `go.mod` files were found:\n\n`%s`", len(goModPaths), strings.Join(goModPaths, "`, `")), severityNote, telemetryOnly, noLocation, ) } func EmitSingleRootGoModFound(goModPath string) { emitDiagnostic( "go/autobuilder/single-root-go-mod-found", "A single `go.mod` file was found in the root", "A single `go.mod` file was found.\n\n`"+goModPath+"`", severityNote, telemetryOnly, noLocation, ) } func EmitSingleNonRootGoModFound(goModPath string) { emitDiagnostic( "go/autobuilder/single-non-root-go-mod-found", "A single, non-root `go.mod` file was found", "A single, non-root `go.mod` file was found.\n\n`"+goModPath+"`", severityNote, telemetryOnly, noLocation, ) } // The following diagnostics are related to identifying the build environment. func EmitNoGoModAndNoGoEnv(msg string) { emitDiagnostic( "go/autobuilder/env-no-go-mod-no-go-env", "No `go.mod` file found and no Go version in environment", msg, severityNote, telemetryOnly, noLocation, ) } func EmitNoGoModAndGoEnvUnsupported(msg string) { emitDiagnostic( "go/autobuilder/env-no-go-mod-go-env-unsupported", "No `go.mod` file found and Go version in environment is unsupported", msg, severityNote, telemetryOnly, noLocation, ) } func EmitNoGoModAndGoEnvSupported(msg string) { emitDiagnostic( "go/autobuilder/env-no-go-mod-go-env-supported", "No `go.mod` file found and Go version in environment is supported", msg, severityNote, telemetryOnly, noLocation, ) } func EmitGoModVersionTooHighAndNoGoEnv(msg string) { emitDiagnostic( "go/autobuilder/env-go-mod-version-too-high-no-go-env", "Go version in `go.mod` file above supported range and no Go version in environment", msg, severityNote, telemetryOnly, noLocation, ) } func EmitGoModVersionTooHighAndEnvVersionTooHigh(msg string) { emitDiagnostic( "go/autobuilder/env-go-mod-version-too-high-go-env-too-high", "Go version in `go.mod` file above supported range and Go version in environment above supported range", msg, severityNote, telemetryOnly, noLocation, ) } func EmitGoModVersionTooHighAndEnvVersionTooLow(msg string) { emitDiagnostic( "go/autobuilder/env-go-mod-version-too-high-go-env-too-low", "Go version in `go.mod` file above supported range and Go version in environment below supported range", msg, severityNote, telemetryOnly, noLocation, ) } func EmitGoModVersionTooHighAndEnvVersionBelowMax(msg string) { emitDiagnostic( "go/autobuilder/env-go-mod-version-too-high-go-env-below-max", "Go version in `go.mod` file above supported range and Go version in environment is supported and below the maximum supported version", msg, severityNote, telemetryOnly, noLocation, ) } func EmitGoModVersionTooHighAndEnvVersionMax(msg string) { emitDiagnostic( "go/autobuilder/env-go-mod-version-too-high-go-env-max", "Go version in `go.mod` file above supported range and Go version in environment is the maximum supported version", msg, severityNote, telemetryOnly, noLocation, ) } func EmitGoModVersionTooLowAndNoGoEnv(msg string) { emitDiagnostic( "go/autobuilder/env-go-mod-version-too-low-no-go-env", "Go version in `go.mod` file below supported range and no Go version in environment", msg, severityNote, telemetryOnly, noLocation, ) } func EmitGoModVersionTooLowAndEnvVersionUnsupported(msg string) { emitDiagnostic( "go/autobuilder/env-go-mod-version-too-low-go-env-unsupported", "Go version in `go.mod` file below supported range and Go version in environment unsupported", msg, severityNote, telemetryOnly, noLocation, ) } func EmitGoModVersionTooLowAndEnvVersionSupported(msg string) { emitDiagnostic( "go/autobuilder/env-go-mod-version-too-low-go-env-supported", "Go version in `go.mod` file below supported range and Go version in environment supported", msg, severityNote, telemetryOnly, noLocation, ) } func EmitGoModVersionSupportedAndNoGoEnv(msg string) { emitDiagnostic( "go/autobuilder/env-go-mod-version-supported-no-go-env", "Go version in `go.mod` file in supported range and no Go version in environment", msg, severityNote, telemetryOnly, noLocation, ) } func EmitGoModVersionSupportedAndGoEnvUnsupported(msg string) { emitDiagnostic( "go/autobuilder/env-go-mod-version-supported-go-env-unsupported", "Go version in `go.mod` file in supported range and Go version in environment unsupported", msg, severityNote, telemetryOnly, noLocation, ) } func EmitGoModVersionSupportedHigherGoEnv(msg string) { emitDiagnostic( "go/autobuilder/env-go-mod-version-supported-higher-than-go-env", "The Go version in `go.mod` file is supported and higher than the Go version in environment", msg, severityNote, telemetryOnly, noLocation, ) } func EmitGoModVersionSupportedLowerEqualGoEnv(msg string) { emitDiagnostic( "go/autobuilder/env-go-mod-version-supported-lower-than-or-equal-to-go-env", "The Go version in `go.mod` file is supported and lower than or equal to the Go version in environment", msg, severityNote, telemetryOnly, noLocation, ) } func EmitNewerSystemGoRequired(requiredVersion string) { emitDiagnostic( "go/autobuilder/newer-system-go-version-required", "The Go version installed on the system is too old to support this project", "At least Go version `"+requiredVersion+"` is required to build this project, but the version installed on the system is older. [Install a newer version](https://github.com/actions/setup-go#basic).", severityError, fullVisibility, noLocation, ) } func EmitExtractionFailedForProjects(path []string) { emitDiagnostic( "go/autobuilder/extraction-failed-for-project", "Unable to extract some Go projects", fmt.Sprintf( "The following %d Go project%s could not be extracted successfully:\n\n`%s`\n", len(path), plural(len(path), "", "s"), strings.Join(path, "`, `")), severityWarning, fullVisibility, noLocation, ) } func EmitPrivateRegistryUsed(writer DiagnosticsWriter, configs []string) { n := len(configs) lines := make([]string, n) for i := range configs { lines[i] = fmt.Sprintf("* %s", configs[i]) } emitDiagnosticTo( writer, "go/autobuilder/analysis-using-private-registries", "Go extraction used private package registries", fmt.Sprintf( "Go was extracted using the following private package registr%s:\n\n%s\n", plural(n, "y", "ies"), strings.Join(lines, "\n")), severityNote, fullVisibility, noLocation, ) }