Skip to content

Commit 6c0c089

Browse files
committed
add 'malicious mods' blacklist
1 parent 8c7dffc commit 6c0c089

File tree

22 files changed

+358
-24
lines changed

22 files changed

+358
-24
lines changed

build/deploy-local-smapi.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ This assumes `find-game-folder.targets` has already been imported and validated.
1919
<Copy SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFolder="$(GamePath)" Condition="$(OS) == 'Windows_NT'" />
2020
<Copy SourceFiles="$(TargetDir)\$(TargetName)" DestinationFolder="$(GamePath)" Condition="$(OS) != 'Windows_NT'" />
2121
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)" />
22+
<Copy SourceFiles="$(TargetDir)\SMAPI.blacklist.json" DestinationFiles="$(GamePath)\smapi-internal\blacklist.json" />
2223
<Copy SourceFiles="$(TargetDir)\SMAPI.config.json" DestinationFiles="$(GamePath)\smapi-internal\config.json" />
2324
<Copy SourceFiles="$(TargetDir)\SMAPI.metadata.json" DestinationFiles="$(GamePath)\smapi-internal\metadata.json" />
2425
<Copy SourceFiles="$(TargetDir)\Markdig.dll" DestinationFolder="$(GamePath)\smapi-internal" />

build/unix/prepare-install-package.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ for folder in ${folders[@]}; do
163163
cp "$smapiBin/$name" "$bundlePath/smapi-internal"
164164
done
165165

166+
cp "$smapiBin/SMAPI.blacklist.json" "$bundlePath/smapi-internal/blacklist.json"
166167
cp "$smapiBin/SMAPI.config.json" "$bundlePath/smapi-internal/config.json"
167168
cp "$smapiBin/SMAPI.metadata.json" "$bundlePath/smapi-internal/metadata.json"
168169
if [ $folder == "linux" ] || [ $folder == "macOS" ]; then

build/windows/prepare-install-package.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ foreach ($folder in $folders) {
187187
cp "$smapiBin/VdfConverter.dll" "$bundlePath/smapi-internal"
188188
}
189189

190+
cp "$smapiBin/SMAPI.blacklist.json" "$bundlePath/smapi-internal/blacklist.json"
190191
cp "$smapiBin/SMAPI.config.json" "$bundlePath/smapi-internal/config.json"
191192
cp "$smapiBin/SMAPI.metadata.json" "$bundlePath/smapi-internal/metadata.json"
192193
if ($folder -eq "linux" -or $folder -eq "macOS") {

docs/release-notes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# Release notes
44
## Upcoming release
55
* For players:
6+
* Added 'malicious mod' blacklist.
7+
_Once a malicious mod has been reported, this lets us quickly block it for all players. This helps mitigate damage in case of future attacks. This feature can be disabled in the SMAPI settings if needed._
68
* Improved content load performance for non-English players.
79
* Fixed some community shortcuts breaking if a mod edited the map which contains them.
810

src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ public void WriteLine(string message, ConsoleLogLevel level)
5555
{
5656
Console.BackgroundColor = ConsoleColor.Red;
5757
Console.ForegroundColor = ConsoleColor.White;
58-
Console.WriteLine(message);
59-
Console.ResetColor();
58+
Console.Write(message);
59+
Console.ResetColor(); // reset color before line break, so we don't apply background color to the next line
60+
Console.WriteLine();
6061
}
6162
else
6263
{

src/SMAPI.Tests/Core/ModResolverTests.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using StardewModdingAPI.Framework;
1111
using StardewModdingAPI.Framework.ModLoading;
1212
using StardewModdingAPI.Toolkit;
13+
using StardewModdingAPI.Toolkit.Framework.ModBlacklistData;
1314
using StardewModdingAPI.Toolkit.Framework.ModData;
1415
using StardewModdingAPI.Toolkit.Serialization.Models;
1516
using StardewModdingAPI.Toolkit.Utilities.PathLookups;
@@ -35,7 +36,7 @@ public void ReadBasicManifest_NoMods_ReturnsEmptyList()
3536
Directory.CreateDirectory(rootFolder);
3637

3738
// act
38-
IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase(), useCaseInsensitiveFilePaths: true).ToArray();
39+
IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModBlacklist(), new ModDatabase(), useCaseInsensitiveFilePaths: true).ToArray();
3940

4041
// assert
4142
mods.Should().BeEmpty("it should match number of mods input");
@@ -53,7 +54,7 @@ public void ReadBasicManifest_EmptyModFolder_ReturnsFailedManifest()
5354
Directory.CreateDirectory(modFolder);
5455

5556
// act
56-
IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase(), useCaseInsensitiveFilePaths: true).ToArray();
57+
IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModBlacklist(), new ModDatabase(), useCaseInsensitiveFilePaths: true).ToArray();
5758
IModMetadata? mod = mods.FirstOrDefault();
5859

5960
// assert
@@ -96,13 +97,13 @@ public void ReadBasicManifest_CanReadFile()
9697
File.WriteAllText(filename, JsonConvert.SerializeObject(original));
9798

9899
// act
99-
IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase(), useCaseInsensitiveFilePaths: true).ToArray();
100+
IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModBlacklist(), new ModDatabase(), useCaseInsensitiveFilePaths: true).ToArray();
100101
IModMetadata? mod = mods.FirstOrDefault();
101102

102103
// assert
103104
mods.Should().HaveCount(1, "it should match number of mods input");
104105
mod.Should().NotBeNull();
105-
mod!.DataRecord.Should().BeNull("we didn't provide one");
106+
mod.DataRecord.Should().BeNull("we didn't provide one");
106107
mod.DirectoryPath.Should().Be(modFolder);
107108
mod.Error.Should().BeNull();
108109
mod.Status.Should().Be(ModMetadataStatus.Found);
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System;
2+
using StardewModdingAPI.Toolkit.Utilities;
3+
4+
namespace StardewModdingAPI.Toolkit.Framework.ModBlacklistData;
5+
6+
/// <summary>Handles access to SMAPI's internal 'malicious mods' blacklist.</summary>
7+
public class ModBlacklist
8+
{
9+
/*********
10+
** Accessors
11+
*********/
12+
/// <summary>The underlying mod blacklist data.</summary>
13+
public ModBlacklistModel Blacklist { get; }
14+
15+
16+
/*********
17+
** Public methods
18+
*********/
19+
/// <summary>Construct an empty instance.</summary>
20+
public ModBlacklist()
21+
: this(new ModBlacklistModel([])) { }
22+
23+
/// <summary>Construct an instance.</summary>
24+
/// <param name="data">The underlying mod blacklist data.</param>
25+
public ModBlacklist(ModBlacklistModel data)
26+
{
27+
this.Blacklist = data;
28+
}
29+
30+
/// <summary>Get the blacklist entry for a mod, if any.</summary>
31+
/// <param name="modId">The unique mod ID.</param>
32+
/// <param name="entryDllPath">The absolute path to the entry DLL, if this is a C# mod.</param>
33+
public ModBlacklistEntryModel? Get(string modId, string? entryDllPath)
34+
{
35+
string? entryDllHash = null;
36+
37+
foreach (ModBlacklistEntryModel entry in this.Blacklist.Blacklist)
38+
{
39+
// check mod ID
40+
if (entry.Id != null && !string.Equals(modId, entry.Id, StringComparison.OrdinalIgnoreCase))
41+
continue;
42+
43+
// check entry DLL hash
44+
if (entry.EntryDllHash != null)
45+
{
46+
if (entryDllPath is null)
47+
continue;
48+
49+
entryDllHash ??= FileUtilities.GetFileHash(entryDllPath);
50+
if (!string.Equals(entryDllHash, entry.EntryDllHash, StringComparison.OrdinalIgnoreCase))
51+
continue;
52+
}
53+
54+
return entry;
55+
}
56+
57+
return null;
58+
}
59+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
namespace StardewModdingAPI.Toolkit.Framework.ModBlacklistData;
2+
3+
/// <summary>A mod entry in the <see cref="ModBlacklistModel"/>.</summary>
4+
public class ModBlacklistEntryModel
5+
{
6+
/// <summary>The manifest IDs to block (if any).</summary>
7+
public string? Id { get; }
8+
9+
/// <summary>The MD5 hashes of the entry DLL to block (if any).</summary>
10+
/// <remarks>Due to the chance of MD5 collisions, this should only be used in addition to the <see cref="Id"/>.</remarks>
11+
public string? EntryDllHash { get; }
12+
13+
/// <summary>A player-friendly explanation of why the mod is blocked and what they should do next.</summary>
14+
public string Message { get; }
15+
16+
17+
/*********
18+
** Public methods
19+
*********/
20+
/// <summary>Construct an instance.</summary>
21+
/// <param name="id"><inheritdoc cref="Id" path="/summary"/></param>
22+
/// <param name="entryDllHash"><inheritdoc cref="EntryDllHash" path="/summary"/></param>
23+
/// <param name="message"><inheritdoc cref="Message" path="/summary"/></param>
24+
public ModBlacklistEntryModel(string? id, string? entryDllHash, string message)
25+
{
26+
this.Id = id;
27+
this.EntryDllHash = entryDllHash;
28+
this.Message = message;
29+
}
30+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace StardewModdingAPI.Toolkit.Framework.ModBlacklistData;
2+
3+
/// <summary>A list of malicious mods which should be blocked by SMAPI.</summary>
4+
public class ModBlacklistModel
5+
{
6+
/*********
7+
** Accessors
8+
*********/
9+
/// <summary>Metadata about malicious or harmful SMAPI mods which are disabled by default.</summary>
10+
public ModBlacklistEntryModel[] Blacklist { get; }
11+
12+
13+
/*********
14+
** Public methods
15+
*********/
16+
/// <summary>Construct an instance.</summary>
17+
/// <param name="blacklist"><inheritdoc cref="Blacklist" path="/summary"/></param>
18+
public ModBlacklistModel(ModBlacklistEntryModel[]? blacklist)
19+
{
20+
this.Blacklist = blacklist ?? [];
21+
}
22+
}

src/SMAPI.Toolkit/ModToolkit.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Newtonsoft.Json;
66
using StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo;
77
using StardewModdingAPI.Toolkit.Framework.GameScanning;
8+
using StardewModdingAPI.Toolkit.Framework.ModBlacklistData;
89
using StardewModdingAPI.Toolkit.Framework.ModData;
910
using StardewModdingAPI.Toolkit.Framework.ModScanning;
1011
using StardewModdingAPI.Toolkit.Framework.UpdateData;
@@ -78,11 +79,21 @@ public async Task<ModCompatibilityEntry[]> GetCompatibilityListFromLocalGitFolde
7879
return await client.FetchModsFromLocalGitFolderAsync(gitRepoPath);
7980
}
8081

82+
/// <summary>Get SMAPI's internal blacklist of malicious or harmful mods.</summary>
83+
/// <param name="path">The file path for the SMAPI blacklist file.</param>
84+
public ModBlacklist GetModBlacklist(string path)
85+
{
86+
ModBlacklistModel? data = JsonConvert.DeserializeObject<ModBlacklistModel>(File.ReadAllText(path));
87+
return data != null
88+
? new ModBlacklist(data)
89+
: new ModBlacklist();
90+
}
91+
8192
/// <summary>Get SMAPI's internal mod database.</summary>
82-
/// <param name="metadataPath">The file path for the SMAPI metadata file.</param>
83-
public ModDatabase GetModDatabase(string metadataPath)
93+
/// <param name="path">The file path for the SMAPI metadata file.</param>
94+
public ModDatabase GetModDatabase(string path)
8495
{
85-
MetadataModel metadata = JsonConvert.DeserializeObject<MetadataModel>(File.ReadAllText(metadataPath)) ?? new MetadataModel();
96+
MetadataModel metadata = JsonConvert.DeserializeObject<MetadataModel>(File.ReadAllText(path)) ?? new MetadataModel();
8697
ModDataRecord[] records = metadata.ModData.Select(pair => new ModDataRecord(pair.Key, pair.Value)).ToArray();
8798
return new ModDatabase(records, this.GetUpdateUrl);
8899
}

0 commit comments

Comments
 (0)