Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions pkg/app/master/command/cliflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@ const (

// Shared command flag names
const (
FlagCommandParamsFile = "command-params-file"
FlagTarget = "target"
FlagPull = "pull"
FlagCommandParamsFile = "command-params-file"
FlagTarget = "target"
FlagTargetImageArchive = "target-image-archive"
FlagPull = "pull"
FlagDockerConfigPath = "docker-config-path"
FlagRegistryAccount = "registry-account"
FlagRegistrySecret = "registry-secret"
Expand Down Expand Up @@ -200,9 +201,10 @@ const (

// Shared command flag usage info
const (
FlagCommandParamsFileUsage = "JSON file with all command parameters"
FlagTargetUsage = "Target container image (name or ID)"
FlagPullUsage = "Try pulling target if it's not available locally"
FlagCommandParamsFileUsage = "JSON file with all command parameters"
FlagTargetUsage = "Target container image (name or ID)"
FlagTargetImageArchiveUsage = "Target container image archive (tar file path)"
FlagPullUsage = "Try pulling target if it's not available locally"
FlagDockerConfigPathUsage = "Docker config path (used to fetch registry credentials)"
FlagRegistryAccountUsage = "Target registry account used when pulling images from private registries"
FlagRegistrySecretUsage = "Target registry secret used when pulling images from private registries"
Expand Down Expand Up @@ -467,6 +469,12 @@ var CommonFlags = map[string]cli.Flag{
Usage: FlagTargetUsage,
EnvVars: []string{"DSLIM_TARGET"},
},
FlagTargetImageArchive: &cli.StringFlag{
Name: FlagTargetImageArchive,
Value: "",
Usage: FlagTargetImageArchiveUsage,
EnvVars: []string{"DSLIM_TARGET_IMAGE_ARCHIVE"},
},
FlagPull: &cli.BoolFlag{
Name: FlagPull,
Value: true, //enabled by default
Expand Down
7 changes: 5 additions & 2 deletions pkg/app/master/command/xray/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ var XRayFlags = []cli.Flag{
command.Cflag(command.FlagRuntime),
command.Cflag(command.FlagCommandParamsFile),
command.Cflag(command.FlagTarget),
command.Cflag(command.FlagTargetImageArchive),
command.Cflag(command.FlagPull),
command.Cflag(command.FlagDockerConfigPath),
command.Cflag(command.FlagRegistryAccount),
Expand Down Expand Up @@ -114,9 +115,10 @@ var CLI = &cli.Command{
}

targetRef := ctx.String(command.FlagTarget)
if targetRef == "" {
targetImageArchive := ctx.String(command.FlagTargetImageArchive)
if targetRef == "" && targetImageArchive == "" {
if ctx.Args().Len() < 1 {
xc.Out.Error("param.target", "missing image ID/name")
xc.Out.Error("param.target", "missing image ID/name or archive path")
cli.ShowCommandHelp(ctx, Name)
return nil
} else {
Expand Down Expand Up @@ -345,6 +347,7 @@ var CLI = &cli.Command{
gcvalues,
cparams,
targetRef,
targetImageArchive,
doPull,
dockerConfigPath,
registryAccount,
Expand Down
98 changes: 93 additions & 5 deletions pkg/app/master/command/xray/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func OnCommand(
gparams *command.GenericParams,
cparams *CommandParams,
targetRef string,
targetImageArchive string,
doPull bool,
dockerConfigPath string,
registryAccount string,
Expand Down Expand Up @@ -162,13 +163,100 @@ func OnCommand(

xc.Out.Info("cmd.input.params",
ovars{
"runtime": rr,
"target": targetRef,
"add-image-manifest": doAddImageManifest,
"add-image-config": doAddImageConfig,
"rm-file-artifacts": doRmFileArtifacts,
"runtime": rr,
"target": targetRef,
"target-image-archive": targetImageArchive,
"add-image-manifest": doAddImageManifest,
"add-image-config": doAddImageConfig,
"rm-file-artifacts": doRmFileArtifacts,
})

// Handle archive-based analysis (when --target-image-archive is provided)
if targetImageArchive != "" {
logger.Debugf("using target image archive: %s", targetImageArchive)

if !fsutil.IsRegularFile(targetImageArchive) {
xc.Out.Error("target.image.archive", "archive file not found")
exitCode := command.ECTCommon | command.ECCImageNotFound
xc.Out.State("exited", ovars{"exit.code": exitCode})
xc.Exit(exitCode)
}

// Get image info from archive
archiveInfo, err := dockerimage.GetArchiveInfo(targetImageArchive)
if err != nil {
xc.Out.Error("target.image.archive", fmt.Sprintf("error reading archive info: %v", err))
xc.Out.State("exited", ovars{"exit.code": -1})
xc.Exit(-1)
}

imageID := archiveInfo.ImageID
logger.Debugf("archive image ID: %s, tags: %v", imageID, archiveInfo.RepoTags)

cmdReport.TargetReference = targetImageArchive
if len(archiveInfo.RepoTags) > 0 {
cmdReport.TargetReference = archiveInfo.RepoTags[0]
}

xc.Out.State("image.data.inspection.start")
xc.Out.Info("image.archive",
ovars{
"path": targetImageArchive,
"image.id": imageID,
"repo.tags": strings.Join(archiveInfo.RepoTags, ","),
})

pp := &dockerimage.ProcessorParams{
DetectIdentities: &dockerimage.DetectOpParam{
Enabled: cparams.DetectIdentities.Enabled,
DumpRaw: cparams.DetectIdentities.DumpRaw,
IsConsoleOut: cparams.DetectIdentities.IsConsoleOut,
IsDirOut: cparams.DetectIdentities.IsDirOut,
OutputPath: cparams.DetectIdentities.OutputPath,
InputParams: cparams.DetectIdentities.InputParams,
},
DetectAllCertFiles: cparams.DetectAllCertFiles,
DetectAllCertPKFiles: cparams.DetectAllCertPKFiles,
}

xc.Out.Info("image.data.inspection.process.image.start")
_, err = dockerimage.LoadPackage(
targetImageArchive,
imageID,
false,
topChangesMax,
doHashData,
doDetectDuplicates,
changeDataHashMatchers,
changePathMatchers,
changeDataMatchers,
utf8Detector,
pp)

if err != nil {
xc.Out.Error("image.data.inspection", fmt.Sprintf("error loading package: %v", err))
xc.Out.State("exited", ovars{"exit.code": -1})
xc.Exit(-1)
}
xc.Out.Info("image.data.inspection.process.image.end")

if utf8Detector != nil {
errutil.FailOn(utf8Detector.Close())
}

xc.Out.State("image.data.inspection.done")

cmdReport.ImageArchiveLocation = targetImageArchive
cmdReport.State = cmd.StateCompleted
cmdReport.Save()

vinfo := <-viChan
version.PrintCheckVersion(xc, "", vinfo)

xc.Out.State("done")
return
}

resolved := command.ResolveAutoRuntime(cparams.Runtime)
logger.Tracef("runtime.handler: rt=%s resolved=%s", cparams.Runtime, resolved)

Expand Down
93 changes: 92 additions & 1 deletion pkg/docker/dockerimage/dockerimage.go
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,72 @@ const (
OCIImageManifestLocation = "ll.oci.imagemanifest"
)

// ArchiveInfo contains basic information extracted from an image archive
type ArchiveInfo struct {
ImageID string
RepoTags []string
}

// GetArchiveInfo extracts basic image information from a Docker image archive
// by reading the manifest.json file. This is useful when you have an archive
// but don't have the image ID.
func GetArchiveInfo(archivePath string) (*ArchiveInfo, error) {
afile, err := os.Open(archivePath)
if err != nil {
log.Errorf("dockerimage.GetArchiveInfo: os.Open error - %v", err)
return nil, err
}
defer afile.Close()

tr := tar.NewReader(afile)
for {
hdr, err := tr.Next()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
log.Errorf("dockerimage.GetArchiveInfo: error reading archive - %v", err)
return nil, err
}

if hdr == nil || hdr.Name == "" {
continue
}

if hdr.Name == "manifest.json" {
var manifests []DockerManifestObject
if err := json.NewDecoder(tr).Decode(&manifests); err != nil {
log.Errorf("dockerimage.GetArchiveInfo: error decoding manifest - %v", err)
return nil, err
}

if len(manifests) == 0 {
return nil, fmt.Errorf("no manifests found in archive")
}

// Extract image ID from config path (e.g., "abc123.json" -> "abc123")
// or for OCI format: "blobs/sha256/DIGEST" -> "sha256:DIGEST"
configPath := manifests[0].Config
var imageID string
if strings.HasPrefix(configPath, "blobs/sha256/") {
// OCI format
digest := strings.TrimPrefix(configPath, "blobs/sha256/")
imageID = "sha256:" + digest
} else {
// Docker v1 format - strip .json extension
imageID = strings.TrimSuffix(configPath, ".json")
}

return &ArchiveInfo{
ImageID: imageID,
RepoTags: manifests[0].RepoTags,
}, nil
}
}

return nil, fmt.Errorf("manifest.json not found in archive")
}

func LoadPackage(archivePath string,
imageID string,
skipObjects bool,
Expand Down Expand Up @@ -1130,10 +1196,35 @@ func LoadPackage(archivePath string,
layerID = hdr.Name
}

// Handle gzip-compressed OCI image layers
// Many Docker/OCI images use gzip-compressed layers with media types like:
// - application/vnd.docker.image.rootfs.diff.tar.gzip
// - application/vnd.oci.image.layer.v1.tar+gzip
var layerReader io.Reader = tr
mediaType, hasMediaType := nonLayerFileNames[hdr.Name]
isGzipByMediaType := hasMediaType && (strings.Contains(mediaType, "gzip") || strings.Contains(mediaType, "+gzip"))

// Try gzip decompression - gzip.NewReader validates the gzip header
gzReader, gzErr := gzip.NewReader(tr)
if gzErr == nil {
layerReader = gzReader
defer gzReader.Close()
if isGzipByMediaType {
log.Debugf("dockerimage.LoadPackage: using gzip decompression for layer '%s' (mediaType: %s)", hdr.Name, mediaType)
} else {
log.Debugf("dockerimage.LoadPackage: auto-detected gzip compression for layer '%s'", hdr.Name)
}
} else if isGzipByMediaType {
// Media type indicates gzip but decompression failed - this is an error
log.Errorf("dockerimage.LoadPackage: gzip decompression failed for layer(%s/%s) with gzip mediaType '%s' - %v", archivePath, hdr.Name, mediaType, gzErr)
return nil, gzErr
}
// else: not gzip compressed, use raw tar reader

layer, err := layerFromStream(
pkg,
hdr.Name,
tar.NewReader(tr),
tar.NewReader(layerReader),
layerID,
topChangesMax,
doHashData,
Expand Down