From e0c3e50fdb5cb527cdfdfc32d4eb21e2cc143eb5 Mon Sep 17 00:00:00 2001 From: M66B Date: Mon, 25 Jul 2022 10:42:42 +0200 Subject: [PATCH] VirusTotal: list virus scanner results --- FAQ.md | 20 +- .../java/eu/faircode/email/VirusTotal.java | 58 +++++- .../eu/faircode/email/AdapterAttachment.java | 101 +++++---- .../eu/faircode/email/AdapterVirusTotal.java | 191 ++++++++++++++++++ .../eu/faircode/email/FragmentOptions.java | 3 +- .../main/res/layout/dialog_virus_total.xml | 101 +++++---- app/src/main/res/layout/item_virus_total.xml | 37 ++++ app/src/main/res/values/strings.xml | 7 +- 8 files changed, 410 insertions(+), 108 deletions(-) create mode 100644 app/src/main/java/eu/faircode/email/AdapterVirusTotal.java create mode 100644 app/src/main/res/layout/item_virus_total.xml diff --git a/FAQ.md b/FAQ.md index 8acc30a579..8b48f39a4d 100644 --- a/FAQ.md +++ b/FAQ.md @@ -4858,21 +4858,15 @@ else you can double tap or long press the marked text to show suggestions. **(181) How do I use VirusTotal?** -[VirusTotal](https://www.virustotal.com/) integration needs to be enabled in the miscellaneous settings. -This will show a *scan* icon button for each attachment. +VirusTotal integration needs to be enabled in the miscellaneous settings and an API key needs to be entered. +To get an API key, you'll need to sign up via the [VirusTotal website](https://www.virustotal.com/). -Without entering an API key, tapping on the scan button will calculate the SHA-256 hash of the attached file and open the corresponding file report on the VirusTotal website. -If the file is not known by VirusTotal ("*Item not found*"), it is probably okay, unless it contains a new virus not being detected by virus scanners yet. +When integration is enabled and an API key is available, a *scan* icon button will be shown for each attachment. +Tapping on the scan button will calculate the SHA-256 hash of the attachment and lookup the file via the VirusTotal API. +If the file is known by VirusTotal, the number of virus scanners considering the file as malicious will be shown. +If the file isn't known by VirusTotal, an upload button will be shown to upload the file for analysis by VirusTotal. -With entering an API key, there will be a dialog showing the number of virus scanners detecting the file. -Tapping on the info button will open the corresponding file report on the VirusTotal website. - -To get an API key, you'll need to register on the VirusTotal website. -You can enter the API key in the miscellaneous settings of the app. - -Note that only the hash of a file will be sent and that files won't be uploaded. - -This feature was added in version 1.1941 and is available in non Play store versions only. +This feature was added in version 1.1942 and is available in non Play store versions of the app only.
diff --git a/app/src/extra/java/eu/faircode/email/VirusTotal.java b/app/src/extra/java/eu/faircode/email/VirusTotal.java index adaafd8f5b..5ff967fc55 100644 --- a/app/src/extra/java/eu/faircode/email/VirusTotal.java +++ b/app/src/extra/java/eu/faircode/email/VirusTotal.java @@ -21,8 +21,12 @@ package eu.faircode.email; import android.content.Context; import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; import android.util.Pair; +import androidx.annotation.NonNull; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -37,6 +41,8 @@ import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.URL; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeoutException; import javax.net.ssl.HttpsURLConnection; @@ -72,25 +78,17 @@ public class VirusTotal { JSONObject jclassification = jattributes.optJSONObject("popular_threat_classification"); String label = (jclassification == null ? null : jclassification.getString("suggested_threat_label")); - int count = 0; - int malicious = 0; + List scanResult = new ArrayList<>(); JSONObject janalysis = jattributes.getJSONObject("last_analysis_results"); JSONArray jnames = janalysis.names(); for (int i = 0; i < jnames.length(); i++) { String name = jnames.getString(i); JSONObject jresult = janalysis.getJSONObject(name); String category = jresult.getString("category"); - //Log.i("VT " + name + "=" + category); - if (!"type-unsupported".equals(category)) - count++; - if ("malicious".equals(category)) - malicious++; + scanResult.add(new ScanResult(name, category)); } - Log.i("VT lookup=" + malicious + "/" + count + " label=" + label); - - result.putInt("count", count); - result.putInt("malicious", malicious); + result.putParcelableArrayList("scans", (ArrayList) scanResult); result.putString("label", label); } @@ -236,4 +234,42 @@ public class VirusTotal { connection.disconnect(); } } + + public static class ScanResult implements Parcelable { + public String name; + public String category; + + ScanResult(String name, String category) { + this.name = name; + this.category = category; + } + + protected ScanResult(Parcel in) { + name = in.readString(); + category = in.readString(); + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int i) { + parcel.writeString(name); + parcel.writeString(category); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public ScanResult createFromParcel(Parcel in) { + return new ScanResult(in); + } + + @Override + public ScanResult[] newArray(int size) { + return new ScanResult[size]; + } + }; + } } diff --git a/app/src/main/java/eu/faircode/email/AdapterAttachment.java b/app/src/main/java/eu/faircode/email/AdapterAttachment.java index 2a7556a540..05ef913957 100644 --- a/app/src/main/java/eu/faircode/email/AdapterAttachment.java +++ b/app/src/main/java/eu/faircode/email/AdapterAttachment.java @@ -21,10 +21,10 @@ package eu.faircode.email; import android.app.Dialog; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Bitmap; +import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; @@ -41,7 +41,6 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.constraintlayout.widget.Group; import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; import androidx.lifecycle.Lifecycle; @@ -51,10 +50,12 @@ import androidx.lifecycle.OnLifecycleEvent; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.ListUpdateCallback; import androidx.recyclerview.widget.RecyclerView; import java.io.File; +import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -71,6 +72,7 @@ public class AdapterAttachment extends RecyclerView.Adapter taskLookup = new SimpleTask() { @Override protected void onPreExecute(Bundle args) { + tvError.setVisibility(View.GONE); pbWait.setVisibility(View.VISIBLE); } @@ -572,23 +589,34 @@ public class AdapterAttachment extends RecyclerView.Adapter scans = result.getParcelableArrayList("scans"); String label = result.getString("label"); - pbAnalysis.setMax(count); - pbAnalysis.setProgress(malicious); - tvCount.setText(malicious + "/" + count); + int malicious = 0; + if (scans != null) + for (VirusTotal.ScanResult scan : scans) + if ("malicious".equals(scan.category)) + malicious++; + + NumberFormat NF = NumberFormat.getNumberInstance(); + + tvUnknown.setVisibility(scans == null ? View.VISIBLE : View.GONE); + tvSummary.setText(getString(R.string.title_vt_summary, NF.format(malicious))); + tvSummary.setTextColor(Helper.resolveColor(context, + malicious == 0 ? android.R.attr.textColorPrimary : R.attr.colorWarning)); + tvSummary.setTypeface(malicious == 0 ? Typeface.DEFAULT : Typeface.DEFAULT_BOLD); + tvSummary.setVisibility(scans == null ? View.GONE : View.VISIBLE); tvLabel.setText(label); - tvLabel.setVisibility(TextUtils.isEmpty(label) ? View.GONE : View.VISIBLE); - tvUnknown.setVisibility(count == 0 ? View.VISIBLE : View.GONE); - btnUpload.setVisibility(count == 0 && !TextUtils.isEmpty(apiKey) ? View.VISIBLE : View.GONE); - grpAnalysis.setVisibility(count == 0 ? View.GONE : View.VISIBLE); + tvReport.setVisibility(scans == null ? View.GONE : View.VISIBLE); + adapter.set(scans == null ? new ArrayList<>() : scans); + rvScan.setVisibility(scans == null ? View.GONE : View.VISIBLE); + btnUpload.setVisibility(scans == null && !TextUtils.isEmpty(apiKey) ? View.VISIBLE : View.GONE); } @Override protected void onException(Bundle args, Throwable ex) { - Log.unexpectedError(getParentFragmentManager(), ex); + tvError.setText(Log.formatThrowable(ex, false)); + tvError.setVisibility(View.VISIBLE); } }; @@ -655,6 +683,13 @@ public class AdapterAttachment extends RecyclerView.Adapter. + + Copyright 2018-2022 by Marcel Bokhorst (M66B) +*/ + +import android.content.Context; +import android.graphics.Typeface; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListUpdateCallback; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class AdapterVirusTotal extends RecyclerView.Adapter { + private Context context; + private LifecycleOwner owner; + private LayoutInflater inflater; + private int colorWarning; + private int textColorSecondary; + + private List items = new ArrayList<>(); + + public class ViewHolder extends RecyclerView.ViewHolder { + private View view; + private TextView tvName; + private TextView tvCategory; + + ViewHolder(View itemView) { + super(itemView); + + view = itemView.findViewById(R.id.clItem); + tvName = itemView.findViewById(R.id.tvName); + tvCategory = itemView.findViewById(R.id.tvCategory); + } + + private void wire() { + } + + private void unwire() { + } + + private void bindTo(VirusTotal.ScanResult scan) { + boolean malicious = "malicious".equals(scan.category); + tvName.setText(scan.name); + tvCategory.setText(scan.category); + tvCategory.setTextColor(malicious ? colorWarning : textColorSecondary); + tvCategory.setTypeface(malicious ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT); + } + } + + AdapterVirusTotal(Context context, LifecycleOwner owner) { + this.context = context; + this.owner = owner; + this.inflater = LayoutInflater.from(context); + this.colorWarning = Helper.resolveColor(context, R.attr.colorWarning); + this.textColorSecondary = Helper.resolveColor(context, android.R.attr.textColorSecondary); + + setHasStableIds(true); + + owner.getLifecycle().addObserver(new LifecycleObserver() { + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + public void onDestroyed() { + Log.d(AdapterVirusTotal.this + " parent destroyed"); + owner.getLifecycle().removeObserver(this); + } + }); + } + + public void set(@NonNull List scans) { + Log.i("Set scans=" + scans.size()); + + DiffUtil.DiffResult diff = DiffUtil.calculateDiff(new DiffCallback(items, scans), false); + + items = scans; + + diff.dispatchUpdatesTo(new ListUpdateCallback() { + @Override + public void onInserted(int position, int count) { + Log.d("Inserted @" + position + " #" + count); + } + + @Override + public void onRemoved(int position, int count) { + Log.d("Removed @" + position + " #" + count); + } + + @Override + public void onMoved(int fromPosition, int toPosition) { + Log.d("Moved " + fromPosition + ">" + toPosition); + } + + @Override + public void onChanged(int position, int count, Object payload) { + Log.d("Changed @" + position + " #" + count); + } + }); + + try { + diff.dispatchUpdatesTo(this); + } catch (Throwable ex) { + Log.e(ex); + } + } + + private static class DiffCallback extends DiffUtil.Callback { + private List prev = new ArrayList<>(); + private List next = new ArrayList<>(); + + DiffCallback(List prev, List next) { + this.prev.addAll(prev); + this.next.addAll(next); + } + + @Override + public int getOldListSize() { + return prev.size(); + } + + @Override + public int getNewListSize() { + return next.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + VirusTotal.ScanResult m1 = prev.get(oldItemPosition); + VirusTotal.ScanResult m2 = next.get(newItemPosition); + return m1.name.equals(m2.name); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + VirusTotal.ScanResult m1 = prev.get(oldItemPosition); + VirusTotal.ScanResult m2 = next.get(newItemPosition); + return (m1.name.equals(m2.name) && + Objects.equals(m1.category, m2.category)); + } + } + + @Override + public long getItemId(int position) { + return items.get(position).name.hashCode(); + } + + @Override + public int getItemCount() { + return items.size(); + } + + @Override + @NonNull + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ViewHolder(inflater.inflate(R.layout.item_virus_total, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + holder.unwire(); + VirusTotal.ScanResult scan = items.get(position); + holder.bindTo(scan); + holder.wire(); + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/faircode/email/FragmentOptions.java b/app/src/main/java/eu/faircode/email/FragmentOptions.java index cde9f01538..37994f0a8d 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOptions.java +++ b/app/src/main/java/eu/faircode/email/FragmentOptions.java @@ -157,7 +157,8 @@ public class FragmentOptions extends FragmentBase { "webview_legacy", "browser_zoom", "fake_dark", "show_recent", "biometrics", - "default_light" + "default_light", + "vt_enabled", "vt_apikey" }; @Override diff --git a/app/src/main/res/layout/dialog_virus_total.xml b/app/src/main/res/layout/dialog_virus_total.xml index 6a4ef6f306..3a0c855553 100644 --- a/app/src/main/res/layout/dialog_virus_total.xml +++ b/app/src/main/res/layout/dialog_virus_total.xml @@ -28,52 +28,23 @@ android:layout_marginTop="24dp" android:text="File name" android:textAppearance="@style/TextAppearance.AppCompat.Medium" + android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tvTitle" /> - - - - - - + app:layout_constraintTop_toBottomOf="@id/tvError" />