From ba197ca7e2251ea580acda661f09f2159d2678cf Mon Sep 17 00:00:00 2001 From: Graeme Porteous Date: Tue, 19 Dec 2023 09:52:51 +0000 Subject: [PATCH 1/4] Extract xlsx hidden data detection Moving into its own module so we can reuse this code for scanning `xls` files and maybe even raw emails too. --- .../lib/excel_analyzer/analyzer.rb | 59 ++----------------- .../lib/excel_analyzer/probe.rb | 58 ++++++++++++++++++ 2 files changed, 64 insertions(+), 53 deletions(-) create mode 100644 gems/excel_analyzer/lib/excel_analyzer/probe.rb diff --git a/gems/excel_analyzer/lib/excel_analyzer/analyzer.rb b/gems/excel_analyzer/lib/excel_analyzer/analyzer.rb index 3ef3d11669..5f1fd3fa1f 100644 --- a/gems/excel_analyzer/lib/excel_analyzer/analyzer.rb +++ b/gems/excel_analyzer/lib/excel_analyzer/analyzer.rb @@ -1,18 +1,16 @@ require "active_storage" require "active_storage/analyzer" -require "nokogiri" -require "zip" + +require "excel_analyzer/probe" module ExcelAnalyzer ## - # The Analyzer class is responsible for analyzing Excel files uploaded through - # Active Storage. It checks for various features within the Excel file such as - # hidden rows, columns, sheets, pivot caches, and external links. - # - # The class uses rubyzip and Nokogiri for reading and parsing the contents of - # the Excel (.xlsx) files. + # The Analyzer class is responsible for analyzing Excel (.xlsx) files uploaded + # through Active Storage. # class Analyzer < ActiveStorage::Analyzer + include ExcelAnalyzer::Probe + XLSX_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" @@ -28,53 +26,8 @@ def metadata def excel_metadata download_blob_to_tempfile(&method(:probe)) - end - - def probe(tempfile) - Zip::File.open(tempfile.path) do |zip_file| - { - pivot_cache: zip_file.glob("xl/pivotCache/*").any?, - external_links: zip_file.glob("xl/externalLinks/*").any?, - hidden_rows: hidden_rows?(zip_file), - hidden_columns: hidden_columns?(zip_file), - hidden_sheets: hidden_sheets?(zip_file) - } - end - rescue StandardError => ex { error: ex.message } end - - def namespace - { "ns" => "http://schemas.openxmlformats.org/spreadsheetml/2006/main" } - end - - def hidden?(object) - object.attr("hidden") == "true" || - object.attr("hidden") == "1" || - object.attr("state") == "hidden" - end - - def hidden_rows?(zip_file) - zip_file.glob("xl/worksheets/*.xml").any? do |worksheet_file| - doc = Nokogiri::XML(worksheet_file.get_input_stream.read) - doc.xpath("//ns:row", namespace).any?(&method(:hidden?)) - end - end - - def hidden_columns?(zip_file) - zip_file.glob("xl/worksheets/*.xml").any? do |worksheet_file| - doc = Nokogiri::XML(worksheet_file.get_input_stream.read) - doc.xpath("//ns:col", namespace).any?(&method(:hidden?)) - end - end - - def hidden_sheets?(zip_file) - workbook_file = zip_file.glob("xl/workbook.xml").first - return false unless workbook_file - - doc = Nokogiri::XML(workbook_file.get_input_stream.read) - doc.xpath("//ns:sheet", namespace).any?(&method(:hidden?)) - end end end diff --git a/gems/excel_analyzer/lib/excel_analyzer/probe.rb b/gems/excel_analyzer/lib/excel_analyzer/probe.rb new file mode 100644 index 0000000000..fcadd2c8ae --- /dev/null +++ b/gems/excel_analyzer/lib/excel_analyzer/probe.rb @@ -0,0 +1,58 @@ +require "nokogiri" +require "zip" + +module ExcelAnalyzer + ## + # It checks for various features within the Excel (.xlsx) file such as + # hidden rows, columns, sheets, pivot caches, and external links. + # + # The module uses rubyzip and Nokogiri for reading and parsing the contents. + # + module Probe + def probe(io) + Zip::File.open(io.path) do |zip_file| + { + pivot_cache: zip_file.glob("xl/pivotCache/*").any?, + external_links: zip_file.glob("xl/externalLinks/*").any?, + hidden_rows: hidden_rows?(zip_file), + hidden_columns: hidden_columns?(zip_file), + hidden_sheets: hidden_sheets?(zip_file) + } + end + end + + private + + def namespace + { "ns" => "http://schemas.openxmlformats.org/spreadsheetml/2006/main" } + end + + def hidden?(object) + object.attr("hidden") == "true" || + object.attr("hidden") == "1" || + object.attr("state") == "hidden" + end + + def hidden_rows?(zip_file) + zip_file.glob("xl/worksheets/*.xml").any? do |worksheet_file| + doc = Nokogiri::XML(worksheet_file.get_input_stream.read) + doc.xpath("//ns:row", namespace).any?(&method(:hidden?)) + end + end + + def hidden_columns?(zip_file) + zip_file.glob("xl/worksheets/*.xml").any? do |worksheet_file| + doc = Nokogiri::XML(worksheet_file.get_input_stream.read) + doc.xpath("//ns:col", namespace).any?(&method(:hidden?)) + end + end + + def hidden_sheets?(zip_file) + workbook_file = zip_file.glob("xl/workbook.xml").first + return false unless workbook_file + + doc = Nokogiri::XML(workbook_file.get_input_stream.read) + doc.xpath("//ns:sheet", namespace).any?(&method(:hidden?)) + end + end +end From 64f9846c41c551e07589d0b4e8b4fcf8e4446a10 Mon Sep 17 00:00:00 2001 From: Graeme Porteous Date: Tue, 19 Dec 2023 09:56:02 +0000 Subject: [PATCH 2/4] Rename ExcelAnalyzer::Analyzer Rename as ExcelAnalyzer::XlsxAnalyzer to distinguish from a new xls analyzer we're creating. --- gems/excel_analyzer/lib/excel_analyzer.rb | 2 +- gems/excel_analyzer/lib/excel_analyzer/railtie.rb | 4 ++-- .../excel_analyzer/{analyzer.rb => xlsx_analyzer.rb} | 6 +++--- .../{analyzer_spec.rb => xlsx_analyzer_spec.rb} | 12 ++++++------ gems/excel_analyzer/spec/spec_helper.rb | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) rename gems/excel_analyzer/lib/excel_analyzer/{analyzer.rb => xlsx_analyzer.rb} (84%) rename gems/excel_analyzer/spec/excel_analyzer/{analyzer_spec.rb => xlsx_analyzer_spec.rb} (84%) diff --git a/gems/excel_analyzer/lib/excel_analyzer.rb b/gems/excel_analyzer/lib/excel_analyzer.rb index 92cb43e5cb..2a27bb11e9 100644 --- a/gems/excel_analyzer/lib/excel_analyzer.rb +++ b/gems/excel_analyzer/lib/excel_analyzer.rb @@ -1,2 +1,2 @@ -require "excel_analyzer/analyzer" +require "excel_analyzer/xlsx_analyzer" require "excel_analyzer/railtie" if defined?(Rails) diff --git a/gems/excel_analyzer/lib/excel_analyzer/railtie.rb b/gems/excel_analyzer/lib/excel_analyzer/railtie.rb index 6c92d905d9..bfa4d135e8 100644 --- a/gems/excel_analyzer/lib/excel_analyzer/railtie.rb +++ b/gems/excel_analyzer/lib/excel_analyzer/railtie.rb @@ -4,9 +4,9 @@ module ExcelAnalyzer ## # This Railtie integrates the gem with Rails by extending ActiveStorage's - # Analyzers with the custom ExcelAnalyzer::Analyzer. + # Analyzers. # class Railtie < Rails::Railtie - config.active_storage.analyzers.prepend ExcelAnalyzer::Analyzer + config.active_storage.analyzers.prepend ExcelAnalyzer::XlsxAnalyzer end end diff --git a/gems/excel_analyzer/lib/excel_analyzer/analyzer.rb b/gems/excel_analyzer/lib/excel_analyzer/xlsx_analyzer.rb similarity index 84% rename from gems/excel_analyzer/lib/excel_analyzer/analyzer.rb rename to gems/excel_analyzer/lib/excel_analyzer/xlsx_analyzer.rb index 5f1fd3fa1f..10bcc82e68 100644 --- a/gems/excel_analyzer/lib/excel_analyzer/analyzer.rb +++ b/gems/excel_analyzer/lib/excel_analyzer/xlsx_analyzer.rb @@ -8,14 +8,14 @@ module ExcelAnalyzer # The Analyzer class is responsible for analyzing Excel (.xlsx) files uploaded # through Active Storage. # - class Analyzer < ActiveStorage::Analyzer + class XlsxAnalyzer < ActiveStorage::Analyzer include ExcelAnalyzer::Probe - XLSX_CONTENT_TYPE = + CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" def self.accept?(blob) - blob.content_type == XLSX_CONTENT_TYPE + blob.content_type == CONTENT_TYPE end def metadata diff --git a/gems/excel_analyzer/spec/excel_analyzer/analyzer_spec.rb b/gems/excel_analyzer/spec/excel_analyzer/xlsx_analyzer_spec.rb similarity index 84% rename from gems/excel_analyzer/spec/excel_analyzer/analyzer_spec.rb rename to gems/excel_analyzer/spec/excel_analyzer/xlsx_analyzer_spec.rb index bdce8360b9..ddf9bf29bd 100644 --- a/gems/excel_analyzer/spec/excel_analyzer/analyzer_spec.rb +++ b/gems/excel_analyzer/spec/excel_analyzer/xlsx_analyzer_spec.rb @@ -2,13 +2,13 @@ require "spec_helper" -RSpec.describe ExcelAnalyzer::Analyzer do +RSpec.describe ExcelAnalyzer::XlsxAnalyzer do describe ".accept?" do - subject { ExcelAnalyzer::Analyzer.accept?(blob) } + subject { ExcelAnalyzer::XlsxAnalyzer.accept?(blob) } context "when the blob is an Excel file" do let(:blob) do - fake_blob(content_type: ExcelAnalyzer::Analyzer::XLSX_CONTENT_TYPE) + fake_blob(content_type: ExcelAnalyzer::XlsxAnalyzer::CONTENT_TYPE) end it { is_expected.to eq true } @@ -21,12 +21,12 @@ end describe "#metadata" do - let(:metadata) { ExcelAnalyzer::Analyzer.new(blob).metadata } + let(:metadata) { ExcelAnalyzer::XlsxAnalyzer.new(blob).metadata } context "when the blob is an Excel file with hidden data" do let(:blob) do fake_blob(io: File.open(File.join(__dir__, "../fixtures/suspect.xlsx")), - content_type: ExcelAnalyzer::Analyzer::XLSX_CONTENT_TYPE) + content_type: ExcelAnalyzer::XlsxAnalyzer::CONTENT_TYPE) end it "detects pivot cache" do @@ -53,7 +53,7 @@ context "when the blob is an Excel file without hidden data" do let(:blob) do fake_blob(io: File.open(File.join(__dir__, "../fixtures/data.xlsx")), - content_type: ExcelAnalyzer::Analyzer::XLSX_CONTENT_TYPE) + content_type: ExcelAnalyzer::XlsxAnalyzer::CONTENT_TYPE) end it "does not detect hidden data" do diff --git a/gems/excel_analyzer/spec/spec_helper.rb b/gems/excel_analyzer/spec/spec_helper.rb index 5fe1b720c1..ec879e9543 100644 --- a/gems/excel_analyzer/spec/spec_helper.rb +++ b/gems/excel_analyzer/spec/spec_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "bundler/setup" -require "excel_analyzer/analyzer" +require "excel_analyzer/xlsx_analyzer" RSpec.configure do |config| # Enable flags like --only-failures and --next-failure From 421b770e8138d2b553acd3d015dc8e7ddfe8c63b Mon Sep 17 00:00:00 2001 From: Graeme Porteous Date: Tue, 19 Dec 2023 10:08:01 +0000 Subject: [PATCH 3/4] Add XLS analyzer Not enabled by default we will start by running this analyzer manually in the Rails console and Sidekiq before to test performance and how good it is at detecting issues before allowing it to run freely in production. --- config/packages | 1 + config/packages.generic | 1 + config/packages.ubuntu-focal | 1 + gems/excel_analyzer/lib/excel_analyzer.rb | 1 + .../lib/excel_analyzer/railtie.rb | 1 + .../lib/excel_analyzer/xls_analyzer.rb | 67 +++++++++++++ .../spec/excel_analyzer/xls_analyzer_spec.rb | 89 ++++++++++++++++++ gems/excel_analyzer/spec/fixtures/data.xls | Bin 0 -> 6144 bytes gems/excel_analyzer/spec/fixtures/suspect.xls | Bin 0 -> 11776 bytes gems/excel_analyzer/spec/spec_helper.rb | 1 + 10 files changed, 162 insertions(+) create mode 100644 gems/excel_analyzer/lib/excel_analyzer/xls_analyzer.rb create mode 100644 gems/excel_analyzer/spec/excel_analyzer/xls_analyzer_spec.rb create mode 100644 gems/excel_analyzer/spec/fixtures/data.xls create mode 100644 gems/excel_analyzer/spec/fixtures/suspect.xls diff --git a/config/packages b/config/packages index 98e142e3f2..ef56bbedda 100644 --- a/config/packages +++ b/config/packages @@ -15,6 +15,7 @@ libicu-dev libmagic-dev libmagickwand-dev libpq-dev +libreoffice-calc-nogui libsqlite3-dev libxml2-dev libxslt1-dev diff --git a/config/packages.generic b/config/packages.generic index afacbe1236..8bdb00b72d 100644 --- a/config/packages.generic +++ b/config/packages.generic @@ -10,6 +10,7 @@ libicu-dev libmagic-dev libmagickwand-dev libpq-dev +libreoffice-calc-nogui libsqlite3-dev libxml2-dev libxslt-dev diff --git a/config/packages.ubuntu-focal b/config/packages.ubuntu-focal index fc9b482da4..6f52e2746d 100644 --- a/config/packages.ubuntu-focal +++ b/config/packages.ubuntu-focal @@ -10,6 +10,7 @@ libicu-dev libmagic-dev libmagickwand-dev libpq-dev +libreoffice-calc-nogui libsqlite3-dev libxml2-dev libxslt-dev diff --git a/gems/excel_analyzer/lib/excel_analyzer.rb b/gems/excel_analyzer/lib/excel_analyzer.rb index 2a27bb11e9..67d7aa55b6 100644 --- a/gems/excel_analyzer/lib/excel_analyzer.rb +++ b/gems/excel_analyzer/lib/excel_analyzer.rb @@ -1,2 +1,3 @@ +require "excel_analyzer/xls_analyzer" require "excel_analyzer/xlsx_analyzer" require "excel_analyzer/railtie" if defined?(Rails) diff --git a/gems/excel_analyzer/lib/excel_analyzer/railtie.rb b/gems/excel_analyzer/lib/excel_analyzer/railtie.rb index bfa4d135e8..f6e3c47e71 100644 --- a/gems/excel_analyzer/lib/excel_analyzer/railtie.rb +++ b/gems/excel_analyzer/lib/excel_analyzer/railtie.rb @@ -8,5 +8,6 @@ module ExcelAnalyzer # class Railtie < Rails::Railtie config.active_storage.analyzers.prepend ExcelAnalyzer::XlsxAnalyzer + config.active_storage.analyzers.prepend ExcelAnalyzer::XlsAnalyzer end end diff --git a/gems/excel_analyzer/lib/excel_analyzer/xls_analyzer.rb b/gems/excel_analyzer/lib/excel_analyzer/xls_analyzer.rb new file mode 100644 index 0000000000..e47ac9987c --- /dev/null +++ b/gems/excel_analyzer/lib/excel_analyzer/xls_analyzer.rb @@ -0,0 +1,67 @@ +require "open3" +require "tempfile" +require "tmpdir" + +require "active_storage" +require "active_storage/analyzer" + +require "excel_analyzer/probe" + +module ExcelAnalyzer + ## + # The Analyzer class is responsible for analyzing Excel (.xls) files uploaded + # through Active Storage. + # + # Files are first converted to XLSX format and then probed for hidden data. + # + class XlsAnalyzer < ActiveStorage::Analyzer + include ExcelAnalyzer::Probe + + CONTENT_TYPE = "application/vnd.ms-excel" + + def self.accept?(blob) + blob.content_type == CONTENT_TYPE + end + + def metadata + { excel: excel_metadata } + end + + private + + def excel_metadata + download_blob_to_tempfile(&method(:convert_and_probe)) + rescue StandardError => ex + { error: ex.message } + end + + def convert_and_probe(io) + probe(convert(io)) + end + + def convert(io) + raise 'LibreOffice (soffice) command not found' unless soffice_installed? + + Dir.mktmpdir do |tmpdir| + _stdout, _stderr, status = Open3.capture3( + "soffice --headless --convert-to xlsx --outdir #{tmpdir} #{io.path}" + ) + + path = File.join(tmpdir, File.basename(io.path, ".*") + ".xlsx") + + if !status.success? || !File.exist?(path) + raise "LibreOffice conversion failed" + end + + Tempfile.new.tap do |tempfile| + tempfile.write(File.read(path)) + tempfile.rewind + end + end + end + + def soffice_installed? + system("which soffice > /dev/null 2>&1") + end + end +end diff --git a/gems/excel_analyzer/spec/excel_analyzer/xls_analyzer_spec.rb b/gems/excel_analyzer/spec/excel_analyzer/xls_analyzer_spec.rb new file mode 100644 index 0000000000..a2ea076595 --- /dev/null +++ b/gems/excel_analyzer/spec/excel_analyzer/xls_analyzer_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe ExcelAnalyzer::XlsAnalyzer do + describe ".accept?" do + subject { ExcelAnalyzer::XlsAnalyzer.accept?(blob) } + + context "when the blob is an Excel file" do + let(:blob) do + fake_blob(content_type: ExcelAnalyzer::XlsAnalyzer::CONTENT_TYPE) + end + + it { is_expected.to eq true } + end + + context "when the blob is not an Excel file" do + let(:blob) { fake_blob(content_type: "text/plain") } + it { is_expected.to eq false } + end + end + + describe "#metadata" do + let(:metadata) { ExcelAnalyzer::XlsAnalyzer.new(blob).metadata } + + context "when the blob is an Excel file with hidden data" do + let(:blob) do + fake_blob(io: File.open(File.join(__dir__, "../fixtures/suspect.xls")), + content_type: ExcelAnalyzer::XlsAnalyzer::CONTENT_TYPE) + end + + it "detects pivot cache" do + expect(metadata[:excel][:pivot_cache]).to eq true + end + + it "detects external links" do + expect(metadata[:excel][:external_links]).to eq true + end + + it "detects hidden rows" do + expect(metadata[:excel][:hidden_rows]).to eq true + end + + it "detects hidden columns" do + expect(metadata[:excel][:hidden_columns]).to eq true + end + + it "detects hidden sheets" do + expect(metadata[:excel][:hidden_sheets]).to eq true + end + end + + context "when the blob is an Excel file without hidden data" do + let(:blob) do + fake_blob(io: File.open(File.join(__dir__, "../fixtures/data.xls")), + content_type: ExcelAnalyzer::XlsAnalyzer::CONTENT_TYPE) + end + + it "does not detect hidden data" do + expect(metadata[:excel]).to eq( + pivot_cache: false, + external_links: false, + hidden_rows: false, + hidden_columns: false, + hidden_sheets: false + ) + end + end + + context "when the blob is not an Excel file" do + let(:blob) do + fake_blob(io: File.open(File.join(__dir__, "../fixtures/plain.txt")), + content_type: "text/plain") + end + + it "returns an error metadata" do + expect(metadata[:excel]).to eq(error: "LibreOffice conversion failed") + end + end + end + + private + + def fake_blob(io: nil, content_type:) + dbl = double(content_type: content_type) + allow(dbl).to receive(:open).and_yield(io) + dbl + end +end diff --git a/gems/excel_analyzer/spec/fixtures/data.xls b/gems/excel_analyzer/spec/fixtures/data.xls new file mode 100644 index 0000000000000000000000000000000000000000..3775c9b1461add0edfe6e8d7605133627b540d34 GIT binary patch literal 6144 zcmeHLTWpj?6h8l6$`%UUmM$s?E>#vvx1<+|7=vtE5J|W+EebItx^4fqE8A|f-NsA2 z6p#lKF&ZCyAu%Mp7>(CNA0)B7`JfR=NRS6pd=m9xAs9mftlxL$U-s|XmMstyOlSAZ z%sDf2&Y3f3X3qSleyo}}_W9EDQj{JOuUyZSOPPyqp`y=h9td zfoqPgTtgog(GTF7&jSPiq|bNCyJ%W-s+MShPD$aHk+5u$A(Ux(39DUtS^|%8iz;QL z`@ATn|IPZ$$uC&oX0~Fx+x|Sid5(JlQ|*7A_uT&r02P3RfJJ~x0ImS30#pMQ18M*v zz!JbcfO`S=0hR)m0qzGZ2dn@*00;wC0%`#d0v-aa0#LuzPRaUO)axC*0p&WUUYPY< z?N%*N9>6M=`Q@91-}A`Vv;RCi;xpe#^^0FY+b6^HjrMr5C;VTfHq{|+(KpB8q#IGQ z%l6Tk^GzIj58+_~m!#pL_is-2|5S ztyY|W35iQmidxa{PPMM^RmgGPuUqXQI~`9>=)O5Ff91lEl27Ad(y5rzFPBEAwWjJn z9i3dK>i_?xABRoDoSu{NcCI&e$QIMrM30x?-}k!L^df&lJ>0M7cr z-?$F0%1?UU^Bw@x&GC#4H}6QNdPjRQ2r4`6{zPgBH%V7KX^#xs>5Ttk2>4tC78E;`P@P2mw%W60bWZ4+M%iOM{cn(hAtN z7dGeVQ>Ate!kSk3>k zR=etJP(Rdg_F%)=?4~+b{g~9^QP_*=@pn+7R@BOm&zF`w1>OO9Rc$ENn+q5|AbA){ zm`lezJpvv&me%QuFhr%0Mg%K7iAJX_reXE}MWaEB`vIRsyQV>o3137EW+9lNK!bUJ zQKG@Tz$nXLK46qWj3Ywi?O^1>2bF0f7xM)stnkUq8zMlN(Q zgOLjZGJ}x|eUn@~%H<0v!!~Ld(vZzyZe6_S8`4nDw2=!}gA)_VbT=w1Nd8^Zu9dEuIQJFvbU5Ch2Dzn>y6trJIzF{{`n zzeLYjT0ar_Z31)aQ-Fo5lxGuvYT;;eJdt;54l<3hy!SQyWD5?!uo{kldZ)b4z`lkb0>e~}MQr7>+X z&PgNf!Pkeh?3Zr+4zb@5XOUOKg0vy_v+ki%hi-K~WLnY=$%F8*UC`nAQitE@NBNu& z_b=0ph8Zdmv~6?REyzuZJwxxAn|S0K$tIt} z*MyV|UPCERrcY}e`8?iBoqrwre(bRH%VtREQ5|gk0yR5*Hr&Geqb~W58n;!<*8cAA NFSjMw8GJkP{{bJAQNjQK literal 0 HcmV?d00001 diff --git a/gems/excel_analyzer/spec/fixtures/suspect.xls b/gems/excel_analyzer/spec/fixtures/suspect.xls new file mode 100644 index 0000000000000000000000000000000000000000..17c88423a78133bb21bb77ff72164d57c99c8906 GIT binary patch literal 11776 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 literal 0 HcmV?d00001 diff --git a/gems/excel_analyzer/spec/spec_helper.rb b/gems/excel_analyzer/spec/spec_helper.rb index ec879e9543..623d6e54d6 100644 --- a/gems/excel_analyzer/spec/spec_helper.rb +++ b/gems/excel_analyzer/spec/spec_helper.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "bundler/setup" +require "excel_analyzer/xls_analyzer" require "excel_analyzer/xlsx_analyzer" RSpec.configure do |config| From 324f4959f8b7773f53e668519a9225fee97a50f0 Mon Sep 17 00:00:00 2001 From: Graeme Porteous Date: Tue, 19 Dec 2023 11:53:20 +0000 Subject: [PATCH 4/4] Update changelog --- doc/CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CHANGES.md b/doc/CHANGES.md index 8a2ea16f9c..24dae806ef 100644 --- a/doc/CHANGES.md +++ b/doc/CHANGES.md @@ -3,7 +3,7 @@ ## Highlighted Features * Add link from incoming message to admin page for attachments (Gareth Rees) -* Add XSLX spreadsheet analyser to automatically detect hidden data (Helen +* Add XLS & XLSX spreadsheet analyser to automatically detect hidden data (Helen Cross, Graeme Porteous) * Update attachment processing to automatically rebuild if cached file goes missing (Graeme Porteous)