From 3f06da5e1e6d9502e1b04e4a46dc066b0b5ffb86 Mon Sep 17 00:00:00 2001 From: Martin Najemi Date: Mon, 16 Feb 2026 16:40:27 +0100 Subject: [PATCH] chore: JSON import taint propagation Risk: low --- CHANGELOG.md | 6 +++ VERSION | 2 +- internal/analyzer/analyzer.go | 89 +++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index feca82f..46995fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.14.0] - 2026-02-14 + +### Added +- JSON import taint propagation: changed `.json` files now taint TS/JS files that import them, with symbol-level granularity based on usage of the imported binding + ## [0.13.0] - 2026-02-14 ### Added @@ -161,6 +166,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Multi-stage Docker build - Automated vendor upgrade workflow +[0.14.0]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.13.0...v0.14.0 [0.13.0]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.12.0...v0.13.0 [0.12.0]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.11.2...v0.12.0 [0.11.2]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.11.1...v0.11.2 diff --git a/VERSION b/VERSION index 54d1a4f..a803cc2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.13.0 +0.14.0 diff --git a/internal/analyzer/analyzer.go b/internal/analyzer/analyzer.go index 55feebc..2f59b47 100644 --- a/internal/analyzer/analyzer.go +++ b/internal/analyzer/analyzer.go @@ -282,6 +282,10 @@ func isStyleImport(source string) bool { return false } +func isJSONImport(source string) bool { + return strings.HasSuffix(strings.ToLower(source), ".json") +} + // isCSSModule returns true if the import source looks like a CSS module file // (e.g. "./Component.module.scss", "./styles.module.css"). func isCSSModule(source string) bool { @@ -496,6 +500,49 @@ func AnalyzeLibraryPackage(projectFolder string, entrypoints []Entrypoint, merge } } + // Seed taint from changed JSON files within this package. + // JSON files are leaf nodes (no imports); if a TS/JS file imports a changed JSON file, + // taint the importing file's symbols based on usage of the imported binding. + changedJSONFiles := make(map[string]bool) + for _, f := range projectChangedFiles { + relToProject := strings.TrimPrefix(f, projectFolder+"/") + if strings.HasSuffix(strings.ToLower(relToProject), ".json") { + changedJSONFiles[relToProject] = true + } + } + if len(changedJSONFiles) > 0 { + for stem, analysis := range fileAnalyses { + for _, imp := range analysis.Imports { + if !strings.HasPrefix(imp.Source, ".") { + continue + } + if !isJSONImport(imp.Source) { + continue + } + fileDir := filepath.Dir(stem + ".ts") + resolved := filepath.Clean(filepath.Join(fileDir, imp.Source)) + if !changedJSONFiles[resolved] { + continue + } + if tainted[stem] == nil { + tainted[stem] = make(map[string]bool) + } + if len(imp.Names) > 0 { + usageTainted := findTaintedSymbolsByUsage(analysis, imp.Names) + for _, s := range usageTainted { + tainted[stem][s] = true + } + debugf(" %s: usage-tainted via JSON import %s (names: %v)", stem, imp.Source, imp.Names) + } else { + for _, sym := range analysis.Symbols { + tainted[stem][sym.Name] = true + } + debugf(" %s: all symbols tainted via JSON import %s", stem, imp.Source) + } + } + } + } + // Seed taint from upstream dependencies (cross-package propagation) if len(upstreamTaint) > 0 { for stem, analysis := range fileAnalyses { @@ -1354,6 +1401,48 @@ func FindAffectedFiles(globPattern string, filterPattern string, upstreamTaint m } } + // Seed from changed JSON files within the project + changedJSONFiles := make(map[string]bool) + for _, f := range changedFiles { + if !strings.HasPrefix(f, projectFolder+"/") { + continue + } + relToProject := strings.TrimPrefix(f, projectFolder+"/") + if strings.HasSuffix(strings.ToLower(relToProject), ".json") { + changedJSONFiles[relToProject] = true + } + } + if len(changedJSONFiles) > 0 { + for stem, analysis := range fileAnalyses { + for _, imp := range analysis.Imports { + if !strings.HasPrefix(imp.Source, ".") { + continue + } + if !isJSONImport(imp.Source) { + continue + } + fileDir := filepath.Dir(stem + ".ts") + resolved := filepath.Clean(filepath.Join(fileDir, imp.Source)) + if !changedJSONFiles[resolved] { + continue + } + if tainted[stem] == nil { + tainted[stem] = make(map[string]bool) + } + if len(imp.Names) > 0 { + usageTainted := findTaintedSymbolsByUsage(analysis, imp.Names) + for _, s := range usageTainted { + tainted[stem][s] = true + } + } else { + for _, sym := range analysis.Symbols { + tainted[stem][sym.Name] = true + } + } + } + } + } + if len(tainted) == 0 { return nil }